refactoring + documentation

This commit is contained in:
ziirish 2016-05-27 16:57:19 +02:00
parent 4db03d82d9
commit b5f7873875
6 changed files with 247 additions and 138 deletions

View file

@ -249,7 +249,7 @@ def init(conf=None, verbose=0, logfile=None, gunicorn=True, unittest=False, debu
red = Redis(host=host, port=port)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = red
app.config['SESSION_USE_SIGNER'] = app.secret_key != None
app.config['SESSION_USE_SIGNER'] = app.secret_key is not None
app.config['SESSION_PERMANENT'] = False
ses = Session()
ses.init_app(app)

View file

@ -12,13 +12,14 @@ import os
import sys
from flask import Blueprint, Response, request
from flask_restplus import Api
from flask_restplus import Api as ApiPlus
from flask_login import current_user
from flask_cache import Cache
from importlib import import_module
from functools import wraps
from .._compat import IS_GUNICORN, PY3
from ..exceptions import BUIserverException
if PY3: # pragma: no cover
basestring = str
@ -125,7 +126,7 @@ def api_login_required(func):
return decorated_view
class ApiWrapper(Api):
class Api(ApiPlus):
"""Wrapper class around :class:`flask_restplus.Api`"""
cache = Cache(config={'CACHE_TYPE': 'null', 'CACHE_NO_NULL_WARNING': True})
loaded = False
@ -157,4 +158,14 @@ class ApiWrapper(Api):
apibp = Blueprint('api', __name__, url_prefix='/api')
api = ApiWrapper(apibp, title='Burp-UI API', description='Burp-UI API to interact with burp', doc='/doc', decorators=[api_login_required])
api = Api(apibp, title='Burp-UI API', description='Burp-UI API to interact with burp', doc='/doc', decorators=[api_login_required])
@api.errorhandler(BUIserverException)
def handle_bui_server_exception(error):
"""Forward a BUIserverException to the final user
:param error: Custom exception
:type error: :class:`burpui.exceptions.BUIserverException`
"""
return {'message': error.description}, error.code

View file

@ -143,11 +143,7 @@ class ServerBackup(Resource):
(not api.bui.acl.is_client_allowed(self.username,
name,
server) and not
self.is_admin and
(to and not
api.bui.acl.is_client_allowed(self.username,
to,
server)))):
self.is_admin)):
self.abort(
403,
'You are not allowed to schedule a backup for this client'

View file

@ -41,12 +41,12 @@ class Restore(Resource):
- ``pass``: password to use for encrypted backups
"""
parser = api.parser()
parser.add_argument('pass', help='Password to use for encrypted backups', location=('form', 'json'), nullable=True)
parser.add_argument('format', required=False, help='Returning archive format', location=('form', 'json'), choices=('zip', 'tar.gz', 'tar.bz2'), default='zip', nullable=True)
parser.add_argument('strip', type=int, help='Number of elements to strip in the path', default=0, location=('form', 'json'), nullable=True)
parser.add_argument('list', required=True, help='List of files/directories to restore', location=('form', 'json'), nullable=False)
parser.add_argument('pass', help='Password to use for encrypted backups', nullable=True)
parser.add_argument('format', required=False, help='Returning archive format', choices=('zip', 'tar.gz', 'tar.bz2'), default='zip', nullable=True)
parser.add_argument('strip', type=int, help='Number of elements to strip in the path', default=0, nullable=True)
parser.add_argument('list', required=True, help='List of files/directories to restore', nullable=False)
# FIXME: the example json seems interpreted during the raise of the exception
# parser.add_argument('list', required=True, help='List of files/directories to restore (example: \'{"restore":[{"folder":true,"key":"/etc"}]}\')', location=('form', 'json'), nullable=False)
# parser.add_argument('list', required=True, help='List of files/directories to restore (example: \'{"restore":[{"folder":true,"key":"/etc"}]}\')', nullable=False)
@ns.expect(parser, validate=True)
@ns.doc(
@ -230,11 +230,11 @@ class ServerRestore(Resource):
- ``restoreto-sc``: restore files on an other client
"""
parser = api.parser()
parser.add_argument('list-sc', required=True, help='List of files/directories to restore', location='form', nullable=False)
parser.add_argument('strip-sc', type=int, help='Number of elements to strip in the path', default=0, location='form', nullable=True)
parser.add_argument('prefix-sc', help='Prefix to the restore path', location='form', nullable=True)
parser.add_argument('force-sc', type=boolean, help='Whether to overwrite existing files', default=False, location='form', nullable=True)
parser.add_argument('restoreto-sc', help='Restore files on an other client', location='form', nullable=True)
parser.add_argument('list-sc', required=True, help='List of files/directories to restore', nullable=False)
parser.add_argument('strip-sc', type=int, help='Number of elements to strip in the path', default=0, nullable=True)
parser.add_argument('prefix-sc', help='Prefix to the restore path', nullable=True)
parser.add_argument('force-sc', type=boolean, help='Whether to overwrite existing files', default=False, nullable=True)
parser.add_argument('restoreto-sc', help='Restore files on an other client', nullable=True)
list_fields = api.model('ListRestoreFiles', {
'key': fields.String(

View file

@ -5,56 +5,55 @@ from . import api, cache_key, parallel_loop
from .custom import fields, Resource
from ..exceptions import BUIserverException
ns = api.namespace('servers', 'Servers methods')
if not api.bui.standalone:
ns = api.namespace('servers', 'Servers methods')
@ns.route('/stats', endpoint='servers_stats')
class ServersStats(Resource):
"""The :class:`burpui.api.servers.ServersStats` resource allows you to
retrieve statistics about servers/agents.
@ns.route('/stats', endpoint='servers_stats')
class ServersStats(Resource):
"""The :class:`burpui.api.servers.ServersStats` resource allows you to
retrieve statistics about servers/agents.
This resource is part of the :mod:`burpui.api.servers` module.
"""
servers_fields = api.model('Servers', {
'alive': fields.Boolean(required=True, description='Is the server reachable'),
'clients': fields.Integer(required=True, description='Number of clients managed by this server'),
'name': fields.String(required=True, description='Server name'),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@ns.marshal_list_with(servers_fields, code=200, description='Success')
@ns.doc(
responses={
500: 'Internal failure',
},
)
def get(self):
"""Returns a list of servers (agents) with basic stats
**GET** method provided by the webservice.
The *JSON* returned is:
::
[
{
'alive': true,
'clients': 2,
'name': 'burp1',
},
{
'alive': false,
'clients': 0,
'name': 'burp2',
},
]
:returns: The *JSON* described above.
This resource is part of the :mod:`burpui.api.servers` module.
"""
servers_fields = api.model('Servers', {
'alive': fields.Boolean(required=True, description='Is the server reachable'),
'clients': fields.Integer(required=True, description='Number of clients managed by this server'),
'name': fields.String(required=True, description='Server name'),
})
r = []
if hasattr(api.bui.cli, 'servers'):
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@ns.marshal_list_with(servers_fields, code=200, description='Success')
@ns.doc(
responses={
500: 'Internal failure',
},
)
def get(self):
"""Returns a list of servers (agents) with basic stats
**GET** method provided by the webservice.
The *JSON* returned is:
::
[
{
'alive': true,
'clients': 2,
'name': 'burp1',
},
{
'alive': false,
'clients': 0,
'name': 'burp2',
},
]
:returns: The *JSON* described above.
"""
r = []
restrict = []
check = False
if api.bui.acl and not self.is_admin:
@ -83,80 +82,78 @@ class ServersStats(Resource):
r = parallel_loop(get_servers_info, api.bui.cli.servers, restrict, check, self.username)
return r
return r
@ns.route('/report', endpoint='servers_report')
class ServersReport(Resource):
"""The :class:`burpui.api.servers.ServersReport` resource allows you to
retrieve a report about servers/agents.
@ns.route('/report', endpoint='servers_report')
class ServersReport(Resource):
"""The :class:`burpui.api.servers.ServersReport` resource allows you to
retrieve a report about servers/agents.
This resource is part of the :mod:`burpui.api.servers` module.
"""
stats_fields = api.model('ServersStats', {
'total': fields.Integer(required=True, description='Number of files', default=0),
'totsize': fields.Integer(required=True, description='Total size occupied by all the backups of this server', default=0),
'linux': fields.Integer(required=True, description='Total number of Linux/Unix clients on this server', default=0),
'windows': fields.Integer(required=True, description='Total number of Windows clients on this server', default=0),
'unknown': fields.Integer(required=True, description='Total number of Unknown clients on this server', default=0),
})
server_fields = api.model('ServersReport', {
'name': fields.String(required=True, description='Server name'),
'stats': fields.Nested(stats_fields, required=True),
})
backup_fields = api.model('ServersBackup', {
'name': fields.String(required=True, description='Server name'),
'number': fields.Integer(required=True, description='Number of backups on this server', default=0),
})
report_fields = api.model('ServersReportFull', {
'backups': fields.Nested(backup_fields, as_list=True, required=True),
'servers': fields.Nested(server_fields, as_list=True, required=True),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@ns.marshal_with(report_fields, code=200, description='Success')
@ns.doc(
responses={
403: 'Insufficient permissions',
500: 'Internal failure',
},
)
def get(self):
"""Returns a global report about all the servers managed by Burp-UI
**GET** method provided by the webservice.
The *JSON* returned is:
::
{
"backups": [
{
"name": "AGENT1",
"number": 49
}
],
"servers": [
{
"name": "AGENT1",
"stats": {
"linux": 4,
"total": 349705,
"totsize": 119400711726,
"unknown": 0,
"windows": 1
}
}
]
}
The output is filtered by the :mod:`burpui.misc.acl` module so that you
only see stats about the clients/servers you are authorized to.
:returns: The *JSON* described above.
This resource is part of the :mod:`burpui.api.servers` module.
"""
r = {}
if hasattr(api.bui.cli, 'servers'):
stats_fields = api.model('ServersStats', {
'total': fields.Integer(required=True, description='Number of files', default=0),
'totsize': fields.Integer(required=True, description='Total size occupied by all the backups of this server', default=0),
'linux': fields.Integer(required=True, description='Total number of Linux/Unix clients on this server', default=0),
'windows': fields.Integer(required=True, description='Total number of Windows clients on this server', default=0),
'unknown': fields.Integer(required=True, description='Total number of Unknown clients on this server', default=0),
})
server_fields = api.model('ServersReport', {
'name': fields.String(required=True, description='Server name'),
'stats': fields.Nested(stats_fields, required=True),
})
backup_fields = api.model('ServersBackup', {
'name': fields.String(required=True, description='Server name'),
'number': fields.Integer(required=True, description='Number of backups on this server', default=0),
})
report_fields = api.model('ServersReportFull', {
'backups': fields.Nested(backup_fields, as_list=True, required=True),
'servers': fields.Nested(server_fields, as_list=True, required=True),
})
@api.cache.cached(timeout=1800, key_prefix=cache_key)
@ns.marshal_with(report_fields, code=200, description='Success')
@ns.doc(
responses={
403: 'Insufficient permissions',
500: 'Internal failure',
},
)
def get(self):
"""Returns a global report about all the servers managed by Burp-UI
**GET** method provided by the webservice.
The *JSON* returned is:
::
{
"backups": [
{
"name": "AGENT1",
"number": 49
}
],
"servers": [
{
"name": "AGENT1",
"stats": {
"linux": 4,
"total": 349705,
"totsize": 119400711726,
"unknown": 0,
"windows": 1
}
}
]
}
The output is filtered by the :mod:`burpui.misc.acl` module so that you
only see stats about the clients/servers you are authorized to.
:returns: The *JSON* described above.
"""
r = {}
restrict = []
check = False
if api.bui.acl and not self.is_admin:
@ -217,4 +214,4 @@ class ServersReport(Resource):
r['backups'] = backups
r['servers'] = servers
return r
return r

View file

@ -9,6 +9,7 @@
"""
from . import api
from .custom import Resource
from .custom.inputs import boolean
from .._compat import unquote
from flask import jsonify, request, url_for
from werkzeug.datastructures import ImmutableMultiDict
@ -28,6 +29,17 @@ class ServerSettings(Resource):
This resource is part of the :mod:`burpui.api.settings` module.
"""
@ns.doc(
params={
'conf': 'Path of the configuration file',
'server': 'Which server to collect data from when in multi-agent mode',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def post(self, conf=None, server=None):
"""Saves the server configuration"""
# Only the admin can edit the configuration
@ -37,6 +49,17 @@ class ServerSettings(Resource):
noti = api.bui.cli.store_conf_srv(request.form, conf, server)
return {'notif': noti}, 200
@ns.doc(
params={
'conf': 'Path of the configuration file',
'server': 'Which server to collect data from when in multi-agent mode',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, conf=None, server=None):
"""Reads the server configuration
@ -193,6 +216,16 @@ class ServerSettings(Resource):
endpoint='clients_list')
class ClientsList(Resource):
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None):
"""Returns a list of clients"""
# Only the admin can edit the configuration
@ -217,6 +250,21 @@ class ClientSettings(Resource):
parser = api.parser()
parser.add_argument('newclient', required=True, help="No 'newclient' provided")
parser_delete = api.parser()
parser_delete.add_argument('revoke', type=boolean, help='Whether to revoke the certificate or not', default=False, nullable=True)
parser_delete.add_argument('delcert', type=boolean, help='Whether to delete the certificate or not', default=False, nullable=True)
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
responses={
200: 'Success',
400: 'Missing parameter',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def put(self, server=None):
"""Creates a new client"""
# Only the admin can edit the configuration
@ -243,6 +291,18 @@ class ClientSettings(Resource):
api.cache.clear()
return {'notif': noti}, 201
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
'client': 'Client name',
'conf': 'Path of the configuration file',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def post(self, server=None, client=None, conf=None):
"""Saves a given client configuration"""
# Only the admin can edit the configuration
@ -252,6 +312,18 @@ class ClientSettings(Resource):
noti = api.bui.cli.store_conf_cli(request.form, client, conf, server)
return {'notif': noti}
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
'client': 'Client name',
'conf': 'Path of the configuration file',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None, client=None, conf=None):
"""Reads a given client configuration"""
# Only the admin can edit the configuration
@ -275,7 +347,19 @@ class ClientSettings(Resource):
'defaults': api.bui.cli.get_parser_attr('defaults', server)
}
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
'client': 'Client name',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def delete(self, server=None, client=None):
"""Deletes a given client"""
# Only the admin can edit the configuration
if api.bui.acl and not self.is_admin:
self.abort(403, 'Sorry, you don\'t have rights to access the setting panel')
@ -295,6 +379,17 @@ class PathExpander(Resource):
parser = api.parser()
parser.add_argument('path', required=True, help="No 'path' provided")
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
'client': 'Client name',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None, client=None):
"""Expends a given path
@ -313,10 +408,20 @@ class PathExpander(Resource):
@ns.route('/options',
'<server>/options',
'/<server>/options',
endpoint='setting_options')
class SettingOptions(Resource):
@ns.doc(
params={
'server': 'Which server to collect data from when in multi-agent mode',
},
responses={
200: 'Success',
403: 'Insufficient permissions',
500: 'Internal failure',
}
)
def get(self, server=None):
"""Returns various setting options"""
if api.bui.acl and not self.is_admin: