def account_bot_crud(account_id, user_id): if flask.request.method in ['GET', 'HEAD']: rec = Bot.get(user_id, account_id) if not rec: return "No such bot", 404 return json.dumps(rec), 200 elif flask.request.method == 'PUT': bot = Bot.forge_from_input(flask.request.get_json(), force_account=account_id, force_id=user_id) rowcount = bot.update() if not rowcount: return "No such bot", 404 mqtt_publish_changed([ 'accounts/{account_id}/bots'.format(account_id=account_id), 'accounts/{account_id}/bots/{bot_id}'.format(account_id=account_id, bot_id=user_id), ]) return "", 204 elif flask.request.method == 'DELETE': # bot should not be able to delete himself, otherwise they could lock themselves out: if int(flask.g.grafolean_data['user_id']) == int(user_id): return "Can't delete yourself", 403 rowcount = Bot.delete(user_id, force_account=account_id) if not rowcount: return "No such bot", 404 mqtt_publish_changed([ 'accounts/{account_id}/bots'.format(account_id=account_id), 'accounts/{account_id}/bots/{bot_id}'.format(account_id=account_id, bot_id=user_id), ]) return "", 204
def users_bots(): """ --- get: summary: Get systemwide bots tags: - Users description: Returns a list of all systemwide bots (bots which are not tied to a specific account). The list is returned in a single array (no pagination). responses: 200: content: application/json: schema: type: object properties: list: type: array items: "$ref": '#/definitions/BotGET' post: summary: Create a systemwide bot tags: - Users description: Creates a systemwide bot. By default, a created bot is without permissions, so they must be granted to it before it can do anything useful. parameters: - name: "body" in: body description: "Bot data" required: true schema: "$ref": '#/definitions/BotPOST' responses: 201: content: application/json: schema: "$ref": '#/definitions/BotGET' """ if flask.request.method in ['GET', 'HEAD']: rec = Bot.get_list() return json.dumps({'list': rec}), 200 elif flask.request.method == 'POST': bot = Bot.forge_from_input(flask.request.get_json()) user_id, _ = bot.insert() rec = Bot.get(user_id, None) mqtt_publish_changed([ 'bots', ]) return json.dumps(rec), 201
def account_bot_permissions(account_id, user_id): """ Allows reading and assigning permissions to account bots (bots which are tied to a specific account). """ # make sure the bot really belongs to the account: rec = Bot.get(user_id, account_id) if not rec: return "No such bot", 404 if flask.request.method in ['GET', 'HEAD']: rec = Permission.get_list(user_id) return json.dumps({'list': rec}), 200 elif flask.request.method == 'POST': granting_user_id = flask.g.grafolean_data['user_id'] permission = Permission.forge_from_input(flask.request.get_json(), user_id) try: permission_id = permission.insert(granting_user_id) mqtt_publish_changed([ 'persons/{}'.format(permission.user_id), 'bots/{}'.format(permission.user_id), ]) return json.dumps({ 'user_id': permission.user_id, 'resource_prefix': permission.resource_prefix, 'methods': permission.methods, 'id': permission_id, }), 201 except AccessDeniedError as ex: return str(ex), 401 except psycopg2.IntegrityError: return "Invalid parameters", 400
def account_bots(account_id): if flask.request.method in ['GET', 'HEAD']: rec = Bot.get_list(account_id) return json.dumps({'list': rec}), 200 elif flask.request.method == 'POST': bot = Bot.forge_from_input(flask.request.get_json(), force_account=account_id) user_id, _ = bot.insert() rec = Bot.get(user_id, account_id) mqtt_publish_changed([ 'accounts/{account_id}/bots'.format(account_id=account_id), 'accounts/{account_id}/bots/{bot_id}'.format(account_id=account_id, bot_id=user_id), ]) return json.dumps(rec), 201
def admin_first_post(): """ --- post: summary: Create first admin user tags: - Admin description: This endpoint helps with setting up a new installation. It allows us to set up just one initial admin access (with name, email and password). Later requests to the same endpoint will fail. At the same time a systemwide (ICMP ping) bot is configured and its token shared via file with a grafolean-ping-bot Docker container (in default setup). parameters: - name: "body" in: body description: "First admin data and credentials" required: true schema: "$ref": '#/definitions/PersonPOST' responses: 201: content: application/json: schema: type: object properties: id: type: integer description: "User id of created admin" 401: description: System already initialized """ if Auth.first_user_exists(): return 'System already initialized', 401 admin = Person.forge_from_input(flask.request.get_json()) admin_id = admin.insert() # make it a superuser: permission = Permission(admin_id, None, None) permission.insert(None, skip_checks=True) # Help users by including a systemwide ping bot in the package by default: Bot.ensure_default_systemwide_bots_exist() mqtt_publish_changed(['status/info']) return json.dumps({ 'id': admin_id, }), 201
def profile(): user_id = flask.g.grafolean_data['user_id'] user_is_bot = flask.g.grafolean_data['user_is_bot'] if user_is_bot: tied_to_account = Bot.get_tied_to_account(user_id) return json.dumps({ 'user_id': user_id, 'user_type': 'bot', 'record': Bot.get(user_id, tied_to_account=tied_to_account), }), 200 else: return json.dumps({ 'user_id': user_id, 'user_type': 'person', 'record': Person.get(user_id), }), 200
def users_bot_token_get(user_id): # make sure the user who is requesting to see the bot token has every permission that this token has, and # also that this user can add the bot: request_user_permissions = Permission.get_list( int(flask.g.grafolean_data['user_id'])) if not Permission.has_all_permissions(request_user_permissions, user_id): return "Not enough permissions to see this bot's token", 401 if not Permission.can_grant_permission(request_user_permissions, 'bots', 'POST'): return "Not enough permissions to see this bot's token - POST to /bots not allowed", 401 token = Bot.get_token(user_id, None) if not token: return "No such bot", 404 return {'token': token}, 200
def account_bot_permission_delete(account_id, user_id, permission_id): """ Revoke permission from account bot """ # make sure the bot really belongs to the account: rec = Bot.get(user_id, account_id) if not rec: return "No such bot", 404 granting_user_id = flask.g.grafolean_data['user_id'] try: rowcount = Permission.delete(permission_id, user_id, granting_user_id) except AccessDeniedError as ex: return str(ex), 401 if not rowcount: return "No such permission", 404 mqtt_publish_changed([ 'persons/{user_id}'.format(user_id=user_id), 'bots/{user_id}'.format(user_id=user_id), ]) return "", 204
def users_bot_crud(user_id): """ --- get: summary: Get bot data tags: - Users description: Returns bot data. parameters: - name: user_id in: path description: "User id" required: true schema: type: integer responses: 200: content: application/json: schema: "$ref": '#/definitions/BotGET' 404: description: No such bot put: summary: Update the bot tags: - Users description: Updates bot name. Note that all other fields are handled automatically (they can't be changed). parameters: - name: user_id in: path description: "User id" required: true schema: type: integer - name: "body" in: body description: "Bot data" required: true schema: "$ref": '#/definitions/BotPOST' responses: 204: description: Update successful 404: description: No such bot delete: summary: Remove the bot tags: - Users description: Removes the bot. Also removes its permissions, if any. parameters: - name: user_id in: path description: "User id" required: true schema: type: integer responses: 204: description: Bot removed successfully 403: description: Can't remove yourself 404: description: No such bot """ if flask.request.method in ['GET', 'HEAD']: rec = Bot.get(user_id, None) if not rec: return "No such bot", 404 return json.dumps(rec), 200 elif flask.request.method == 'PUT': bot = Bot.forge_from_input(flask.request.get_json(), force_id=user_id) rowcount = bot.update() if not rowcount: return "No such bot", 404 mqtt_publish_changed([ 'bots/{user_id}'.format(user_id=user_id), 'bots', ]) return "", 204 elif flask.request.method == 'DELETE': # bot should not be able to delete himself, otherwise they could lock themselves out: if int(flask.g.grafolean_data['user_id']) == int(user_id): return "Can't delete yourself", 403 rowcount = Bot.delete(user_id) if not rowcount: return "No such bot", 404 mqtt_publish_changed([ 'bots/{user_id}'.format(user_id=user_id), 'bots', ]) return "", 204
def before_request(): # http://flask.pocoo.org/docs/1.0/api/#application-globals flask.g.grafolean_data = {} if not flask.request.endpoint in app.view_functions: # Calling /api/admin/migratedb with GET (instead of POST) is a common mistake, so it deserves a warning in the log: if flask.request.path == '/api/admin/migratedb' and flask.request.method == 'GET': log.warning("Did you want to use POST instead of GET?") return "Resource not found", 404 # Browser might (if frontend and backend are not on the same origin) send a pre-flight OPTIONS request to get the # CORS settings. In this case 'Authorization' header will not be set, which could lead to 401 response, which browser # doesn't like. So let's just return 200 on all OPTIONS: if flask.request.method == 'OPTIONS': # we need to set 'Allow' header to notify caller which methods are available: methods = set() for rule in app.url_map.iter_rules(): if flask.request.url_rule == rule: methods |= rule.methods response = flask.make_response('', 200) response.headers['Allow'] = ",".join(sorted(methods)) return response if flask.request.method in ['GET', 'HEAD', 'POST']: # While it is true that CORS is client-side protection, the rules about preflights allow these 3 types of requests # to be sent to the server without OPTIONS preflight - which means that browser will learn about violation too late. # To combat this, we still check Origin header and explicitly deny non-whitelisted requests: origin_header = flask.request.headers.get('Origin', None) if origin_header: # is it a cross-origin request? # still, we sometimes get origin header even if it is not a cross-origin request, so let's double check that we # indeed are doing CORS: if flask.request.url_root.rstrip('/') != origin_header: if origin_header not in CORS_DOMAINS and flask.request.path != '/api/status/info': # this path is an exception return 'CORS not allowed for this origin', 403 if dbutils.db is None: dbutils.db_connect() if dbutils.db is None: # oops, DB error... we should return 5xx: return 'Service unavailable', 503 view_func = app.view_functions[flask.request.endpoint] # unless we have explicitly used @noauth decorator, do authorization check here: if not hasattr(view_func, '_noauth'): try: user_id = None user_is_bot = False authorization_header = flask.request.headers.get('Authorization') query_params_bot_token = flask.request.args.get('b') if authorization_header is not None: received_jwt = JWT.forge_from_authorization_header(authorization_header, allow_leeway=0) flask.g.grafolean_data['jwt'] = received_jwt user_id = received_jwt.data['user_id'] elif query_params_bot_token is not None: user_id = Bot.authenticate_token(query_params_bot_token) user_is_bot = True if user_id is None: log.info("Authentication failed (no such user)") return "Access denied", 401 # check permissions: resource = flask.request.path[len('/api/'):] resource = resource.rstrip('/') is_allowed = Permission.is_access_allowed( user_id=user_id, resource=resource, method=flask.request.method, ) if not is_allowed: log.info("Access denied (permissions check failed) {} {} {}".format(user_id, resource, flask.request.method)) return "Access to resource denied, insufficient permissions", 403 flask.g.grafolean_data['user_id'] = user_id flask.g.grafolean_data['user_is_bot'] = user_is_bot except AuthFailedException as ex: log.info(f"Authentication failed: {str(ex)}") return "Access denied", 401 except: log.exception("Exception while checking access rights") return "Could not validate access", 500