diff options
Diffstat (limited to 'inventory')
-rw-r--r-- | inventory/__init__.py | 13 | ||||
-rw-r--r-- | inventory/api.py | 322 | ||||
-rw-r--r-- | inventory/schema.py | 40 |
3 files changed, 0 insertions, 375 deletions
diff --git a/inventory/__init__.py b/inventory/__init__.py deleted file mode 100644 index 7c12b51..0000000 --- a/inventory/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from flask import Flask - -app = Flask(__name__) -app.config.from_pyfile('inventory.cfg') - -with app.app_context(): - from .api import app as api - app.register_blueprint(api, url_prefix='/api') - - -@app.route('/') -def index(): - return 'foo' diff --git a/inventory/api.py b/inventory/api.py deleted file mode 100644 index 91872f5..0000000 --- a/inventory/api.py +++ /dev/null @@ -1,322 +0,0 @@ -import datetime -import functools -import time - -from bson import ObjectId -from flask import Blueprint, jsonify, current_app, request, abort, url_for, g -from flask_pymongo import PyMongo -import itsdangerous -import jwt -from marshmallow import ValidationError -import pymongo -import pytz -import requests -import yarl - -from .schema import NodeSchema - -# Setup PyMongo -mongo = PyMongo(current_app, tz_aware=True) -mongo.db.nodes.create_index([('fields.value', pymongo.TEXT), ('name', pymongo.TEXT)], name='fields.value_text_name_text') -mongo.db.nodes.create_index([('parent_id', pymongo.ASCENDING)], name='parent_id') -mongo.db.nodes.create_index([('user_id', pymongo.ASCENDING)], name='user_id') - -app = Blueprint('api', __name__) - - -# Error handling -@app.errorhandler(400) -def on_invalid_request(e): - return jsonify({'message': e.description or 'Invalid request'}), 400 - - -@app.errorhandler(403) -def on_forbidden(e): - return jsonify({'message': e.description or 'Forbidden'}), 403 - - -@app.errorhandler(404) -def on_not_found(e): - return jsonify({'message': e.description or 'Not found'}), 404 - - -@app.errorhandler(500) -def on_internal_error(e): - return jsonify({'message': e.description or 'Invalid request'}), 500 - - -@app.errorhandler(ValidationError) -def on_validation_error(e): - return jsonify({'message': 'Validation error', 'fields': e.messages}), 400 - - -def auth_required(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - authorization = request.headers.get('Authorization') - if not authorization: - abort(403) - auth_type, access_token = authorization.split(None, 1) - if auth_type != 'Bearer': - abort(403) - try: - token = jwt.decode(access_token, key=current_app.config['JWT_PUBLIC_KEY'], - audience=current_app.config['OAUTH_CLIENT_ID']) - except jwt.InvalidTokenError: - abort(403) - user_id = ObjectId(token['sub']) - user = mongo.db.users.find_one({'_id': user_id}) - if not user: - abort(403) - g.user = user - return f(*args, **kwargs) - return wrapper - - -def format_node(node): - parents = dict((parent['_id'], parent) for parent in node.pop('parents')) - current = node - # Recursively assign each node their respective parent - while current: - # NOTE: Python assigns from left to right - current['parent'] = current = parents.get(current.get('parent_id')) - - children = dict((child['_id'], child) for child in node.pop('children')) - children_list = node['children'] = [] - for child in children.values(): - # Direct children should be assigned to the top-most list - if child['parent_id'] == node['_id']: - children_list.append(child) - continue - # Otherwise assign to their respective parent - parent = children[child['parent_id']] - parent.setdefault('children', []).append(child) - - return node - -def format_nodes(nodes): - return (format_node(node) for node in nodes) - -# Routes -@app.route('/nodes') -@auth_required -def root_nodes(): - result = mongo.db.nodes.aggregate([ - {'$match': {'parent_id': None, 'user_id': g.user['_id']}}, - { - '$graphLookup': { - 'from': 'nodes', - 'startWith': '$parent_id', - 'connectFromField': 'parent_id', - 'connectToField': '_id', - 'as': 'parents', - }, - }, - { - '$graphLookup': { - 'from': 'nodes', - 'startWith': '$_id', - 'connectFromField': '_id', - 'connectToField': 'parent_id', - 'as': 'children', - 'maxDepth': 1, - }, - }, - ]) - schema = NodeSchema(many=True) - data = schema.dump(format_nodes(result)) - return jsonify(data) - - -@app.route('/nodes', methods=['POST']) -@auth_required -def add_node(): - data = request.json - if data is None or not isinstance(data, dict): - abort(400, 'Payload must be a JSON object') - schema = NodeSchema() - node = schema.load(data) - node['created_at'] = pytz.utc.localize(datetime.datetime.utcnow()) - node['user_id'] = g.user['_id'] - result = mongo.db.nodes.insert_one(node) - if not result.acknowledged: - abort(500, 'Write operation not acknowledged') - node_id = result.inserted_id - return jsonify({'id': str(node_id), 'url': url_for('.node', node_id=node_id)}), 201 - - -@app.route('/nodes/<ObjectId:node_id>') -@auth_required -def node(node_id): - result = mongo.db.nodes.aggregate([ - {'$match': {'_id': node_id, 'user_id': g.user['_id']}}, - { - '$graphLookup': { - 'from': 'nodes', - 'startWith': '$parent_id', - 'connectFromField': 'parent_id', - 'connectToField': '_id', - 'as': 'parents', - }, - }, - { - '$graphLookup': { - 'from': 'nodes', - 'startWith': '$_id', - 'connectFromField': '_id', - 'connectToField': 'parent_id', - 'as': 'children', - 'maxDepth': 1, - }, - }, - ]) - try: - node = result.next() - except StopIteration: - abort(404, 'No node found') - - node = format_node(node) - - schema = NodeSchema() - return jsonify(schema.dump(node)) - - -@app.route('/nodes/<ObjectId:node_id>', methods=['PUT']) -@auth_required -def update_node(node_id): - data = request.json - if data is None or not isinstance(data, dict): - abort(400, 'Payload must be a JSON object') - schema = NodeSchema() - node = schema.load(data) - node['updated_at'] = pytz.utc.localize(datetime.datetime.utcnow()) - result = mongo.db.nodes.update_one({'_id': node_id, 'user_id': g.user['_id']}, {'$set': node}) - if not result.acknowledged: - abort(500, 'Write operation not acknowledged') - return '', 204 - - -@app.route('/nodes/<ObjectId:node_id>', methods=['DELETE']) -@auth_required -def delete_node(node_id): - result = mongo.db.nodes.delete_one({'_id': node_id, 'user_id': g.user['_id']}) - if result.deleted_count == 0: - abort(404, 'No node found') - return jsonify({}), 204 - - -@app.route('/search', methods=['POST']) -@auth_required -def find_nodes(): - if 'q' not in request.form: - abort(400, 'Missing q argument') - schema = NodeSchema(many=True) - data = schema.dump(mongo.db.nodes.find({'$text': {'$search': request.form['q']}, 'user_id': g.user['_id']})) - return jsonify(data) - - -@app.route('/auth/request', methods=['POST']) -def auth_request(): - signer = itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY']) - state = signer.dumps(int(time.time())) - redirect_uri = request.form.get('redirect_uri') - url = (yarl.URL(current_app.config['OAUTH_URL']) / 'authorize').with_query( - response_type='code', - client_id=current_app.config['OAUTH_CLIENT_ID'], - redirect_uri=str(redirect_uri), - state=state, - ) - return jsonify(url=str(url)) - - -def set_refresh_token_cookie(response, refresh_token='', clear=False): - response.set_cookie(key='refresh_token', value=refresh_token, path=url_for('.auth_refresh'), - max_age=0 if clear else (86400 * 30), samesite='strict', httponly=True, secure=request.scheme == 'https') - - -@app.route('/auth/response', methods=['POST']) -def auth_response(): - code = request.form.get('code') - state = request.form.get('state') - signer = itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY']) - try: - signer.loads(state) - except itsdangerous.exc.BadSignature: - abort(500, 'Invalid state') - redirect_uri = request.form.get('redirect_uri') - url = yarl.URL(current_app.config['OAUTH_URL']) / 'token' - response = requests.post(str(url), data={ - 'grant_type': 'authorization_code', - 'code': code, - 'client_id': current_app.config['OAUTH_CLIENT_ID'], - 'client_secret': current_app.config['OAUTH_CLIENT_SECRET'], - 'redirect_uri': redirect_uri, - }) - token = response.json() - error = token.get('error') - if error: - abort(500, error) - - token_data = jwt.decode(token['access_token'], key=current_app.config['JWT_PUBLIC_KEY'], - audience=current_app.config['OAUTH_CLIENT_ID']) - # We're assuming sub is an ObjectId (this is true for jab) - user_id = ObjectId(token_data['sub']) - user = mongo.db.users.find_one({'_id': user_id}) - if not user: - url = yarl.URL(current_app.config['OAUTH_URL']).parent / 'api' / 'user' - response = requests.get(str(url), headers={ - 'Authorization': 'Bearer {}'.format(token['access_token']), - }) - userdata = response.json() - user = { - '_id': user_id, - 'username': userdata['username'], - } - result = mongo.db.users.insert_one(user) - if not result.acknowledged: - abort(500, 'Write not acknowledged') - - r = jsonify(access_token=token['access_token']) - set_refresh_token_cookie(r, token['refresh_token']) - return r - - -@app.route('/auth/refresh', methods=['POST']) -def auth_refresh(): - refresh_token = request.cookies.get('refresh_token') - if not refresh_token: - abort(403) - url = yarl.URL(current_app.config['OAUTH_URL']) / 'token' - response = requests.post(str(url), data={ - 'grant_type': 'refresh_token', - 'client_id': current_app.config['OAUTH_CLIENT_ID'], - 'client_secret': current_app.config['OAUTH_CLIENT_SECRET'], - 'refresh_token': refresh_token, - }) - if not response.ok: - abort(403) - data = response.json() - r = jsonify(access_token=data['access_token']) - set_refresh_token_cookie(r, data['refresh_token']) - return r - - -@app.route('/auth/logout', methods=['POST']) -def auth_logout(): - r = jsonify() - set_refresh_token_cookie(r, clear=True) - return r - - -@app.route('/user') -@auth_required -def user_info(): - url = yarl.URL(current_app.config['OAUTH_URL']).parent / 'api' / 'user' - response = requests.get(str(url), headers={'Authorization': request.headers.get('Authorization')}) - data = response.json() - return jsonify(username=data['username']) - - -@app.route('/') -def index(): - return 'api' diff --git a/inventory/schema.py b/inventory/schema.py deleted file mode 100644 index 0728aea..0000000 --- a/inventory/schema.py +++ /dev/null @@ -1,40 +0,0 @@ -import bson -from flask import url_for -from marshmallow import Schema, fields - -# Map ObjectId to String -Schema.TYPE_MAPPING[bson.ObjectId] = fields.String - - -class ObjectId(fields.Field): - def _serialize(self, value, attr, obj, **kwargs): - if value is None: - return value - return str(value) - - def _deserialize(self, value, attr, data, **kwargs): - if value is None: - return value - return bson.ObjectId(value) - - -class FieldSchema(Schema): - name = fields.String(required=True) - value = fields.String(required=True) - # type - - -class NodeSchema(Schema): - _id = ObjectId(dump_only=True, data_key='id') - name = fields.String(required=True) - parent_id = ObjectId(default=None, allow_none=True) - _fields = fields.List(fields.Nested(FieldSchema()), default=[], attribute='fields', data_key='fields') - - # These are not set by the caller, but by the API endpoint - created_at = fields.AwareDateTime(dump_only=True) - updated_at = fields.AwareDateTime(dump_only=True) - - # Not actual stored fields - parent = fields.Nested(lambda: NodeSchema(), dump_only=True) - children = fields.List(fields.Nested(lambda: NodeSchema()), dump_only=True) - url = fields.Function(lambda obj: url_for('.node', node_id=obj['_id']), dump_only=True) |