Skip to content
Snippets Groups Projects
Commit 77468350 authored by Benjamin Bertrand's avatar Benjamin Bertrand
Browse files

Refactor API

Split API in different blueprints like for the web UI:
- users (login)
- inventory
- network

Use version in API url (api/v1) to make future changes easier without
breaking backward compatibility
parent 9295df9b
No related branches found
No related tags found
No related merge requests found
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
app.api.items app.api.inventory
~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
This module implements the application API. This module implements the inventory API.
:copyright: (c) 2017 European Spallation Source ERIC :copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details. :license: BSD 2-Clause, see LICENSE for more details.
""" """
import sqlalchemy as sa from flask import Blueprint, jsonify, request
from flask import (current_app, Blueprint, jsonify, request)
from flask_jwt_extended import jwt_required from flask_jwt_extended import jwt_required
from flask_ldap3_login import AuthenticationResponseStatus from .. import utils, models
from ..extensions import ldap_manager, db
from ..models import Item, Manufacturer, Model, Location, Status, Action, Network, Interface
from .. import utils, tokens
from ..decorators import jwt_groups_accepted from ..decorators import jwt_groups_accepted
from .utils import commit, create_generic_model, get_generic_model
bp = Blueprint('api', __name__) bp = Blueprint('inventory_api', __name__)
def commit():
try:
db.session.commit()
except (sa.exc.IntegrityError, sa.exc.DataError) as e:
db.session.rollback()
raise utils.CSEntryError(str(e), status_code=422)
def get_item_by_id_or_ics_id(id_): def get_item_by_id_or_ics_id(id_):
...@@ -35,76 +24,20 @@ def get_item_by_id_or_ics_id(id_): ...@@ -35,76 +24,20 @@ def get_item_by_id_or_ics_id(id_):
item_id = int(id_) item_id = int(id_)
except ValueError: except ValueError:
# Assume id_ is an ics_id # Assume id_ is an ics_id
item = Item.query.filter_by(ics_id=id_).first() item = models.Item.query.filter_by(ics_id=id_).first()
else: else:
item = Item.query.get(item_id) item = models.Item.query.get(item_id)
if item is None: if item is None:
raise utils.CSEntryError(f"Item id '{id_}' not found", status_code=404) raise utils.CSEntryError(f"Item id '{id_}' not found", status_code=404)
return item return item
def get_generic_model(model, args):
"""Return data from model as json
:param model: model class
:param MultiDict args: args from the request
:returns: data from model as json
"""
items = model.query.order_by(model.name)
qrcode = args.get('qrcode', 'false').lower() == 'true'
data = [item.to_dict(qrcode=qrcode) for item in items]
return jsonify(data)
def create_generic_model(model, mandatory_fields=('name',)):
data = request.get_json()
if data is None:
raise utils.CSEntryError('Body should be a JSON object')
current_app.logger.debug(f'Received: {data}')
for mandatory_field in mandatory_fields:
if mandatory_field not in data:
raise utils.CSEntryError(f"Missing mandatory field '{mandatory_field}'", status_code=422)
try:
instance = model(**data)
except TypeError as e:
message = str(e).replace('__init__() got an ', '')
raise utils.CSEntryError(message, status_code=422)
except ValueError as e:
raise utils.CSEntryError(str(e), status_code=422)
db.session.add(instance)
commit()
return jsonify(instance.to_dict()), 201
@bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
if data is None:
raise utils.CSEntryError('Body should be a JSON object')
try:
username = data['username']
password = data['password']
except KeyError:
raise utils.CSEntryError('Missing mandatory field (username or password)', status_code=422)
response = ldap_manager.authenticate(username, password)
if response.status == AuthenticationResponseStatus.success:
current_app.logger.debug(f'{username} successfully logged in')
user = ldap_manager._save_user(
response.user_dn,
response.user_id,
response.user_info,
response.user_groups)
payload = {'access_token': tokens.generate_access_token(identity=user.id)}
return jsonify(payload), 200
raise utils.CSEntryError('Invalid credentials', status_code=401)
@bp.route('/items') @bp.route('/items')
@jwt_required @jwt_required
def get_items(): def get_items():
# TODO: add pagination # TODO: add pagination
query = utils.get_query(Item.query, request.args) query = utils.get_query(models.Item.query, request.args)
items = query.order_by(Item._created) items = query.order_by(models.Item._created)
data = [item.to_dict() for item in items] data = [item.to_dict() for item in items]
return jsonify(data) return jsonify(data)
...@@ -126,7 +59,7 @@ def create_item(): ...@@ -126,7 +59,7 @@ def create_item():
# an item so ics_id should also be a mandatory field. # an item so ics_id should also be a mandatory field.
# But there are existing items (in confluence and JIRA) that we want to # But there are existing items (in confluence and JIRA) that we want to
# import and associate after they have been created. # import and associate after they have been created.
return create_generic_model(Item, mandatory_fields=('serial_number',)) return create_generic_model(models.Item, mandatory_fields=('serial_number',))
@bp.route('/items/<id_>', methods=['PATCH']) @bp.route('/items/<id_>', methods=['PATCH'])
...@@ -161,13 +94,13 @@ def patch_item(id_): ...@@ -161,13 +94,13 @@ def patch_item(id_):
item.ics_id = data.get('ics_id') item.ics_id = data.get('ics_id')
elif 'ics_id' in data: elif 'ics_id' in data:
raise utils.CSEntryError("'ics_id' can't be changed", status_code=422) raise utils.CSEntryError("'ics_id' can't be changed", status_code=422)
item.manufacturer = utils.convert_to_model(data.get('manufacturer', item.manufacturer), Manufacturer) item.manufacturer = utils.convert_to_model(data.get('manufacturer', item.manufacturer), models.Manufacturer)
item.model = utils.convert_to_model(data.get('model', item.model), Model) item.model = utils.convert_to_model(data.get('model', item.model), models.Model)
item.location = utils.convert_to_model(data.get('location', item.location), Location) item.location = utils.convert_to_model(data.get('location', item.location), models.Location)
item.status = utils.convert_to_model(data.get('status', item.status), Status) item.status = utils.convert_to_model(data.get('status', item.status), models.Status)
parent_ics_id = data.get('parent') parent_ics_id = data.get('parent')
if parent_ics_id is not None: if parent_ics_id is not None:
parent = Item.query.filter_by(ics_id=parent_ics_id).first() parent = models.Item.query.filter_by(ics_id=parent_ics_id).first()
if parent is not None: if parent is not None:
item.parent_id = parent.id item.parent_id = parent.id
# Update location and status with those from parent # Update location and status with those from parent
...@@ -184,93 +117,56 @@ def patch_item(id_): ...@@ -184,93 +117,56 @@ def patch_item(id_):
@bp.route('/actions') @bp.route('/actions')
@jwt_required @jwt_required
def get_actions(): def get_actions():
return get_generic_model(Action, request.args) return get_generic_model(models.Action, request.args)
@bp.route('/manufacturers') @bp.route('/manufacturers')
@jwt_required @jwt_required
def get_manufacturers(): def get_manufacturers():
return get_generic_model(Manufacturer, request.args) return get_generic_model(models.Manufacturer, request.args)
@bp.route('/manufacturers', methods=['POST']) @bp.route('/manufacturers', methods=['POST'])
@jwt_required @jwt_required
@jwt_groups_accepted('admin', 'create') @jwt_groups_accepted('admin', 'create')
def create_manufacturer(): def create_manufacturer():
return create_generic_model(Manufacturer) return create_generic_model(models.Manufacturer)
@bp.route('/models') @bp.route('/models')
@jwt_required @jwt_required
def get_models(): def get_models():
return get_generic_model(Model, request.args) return get_generic_model(models.Model, request.args)
@bp.route('/models', methods=['POST']) @bp.route('/models', methods=['POST'])
@jwt_required @jwt_required
@jwt_groups_accepted('admin', 'create') @jwt_groups_accepted('admin', 'create')
def create_model(): def create_model():
return create_generic_model(Model) return create_generic_model(models.Model)
@bp.route('/locations') @bp.route('/locations')
@jwt_required @jwt_required
def get_locations(): def get_locations():
return get_generic_model(Location, request.args) return get_generic_model(models.Location, request.args)
@bp.route('/locations', methods=['POST']) @bp.route('/locations', methods=['POST'])
@jwt_required @jwt_required
@jwt_groups_accepted('admin', 'create') @jwt_groups_accepted('admin', 'create')
def create_locations(): def create_locations():
return create_generic_model(Location) return create_generic_model(models.Location)
@bp.route('/status') @bp.route('/status')
@jwt_required @jwt_required
def get_status(): def get_status():
return get_generic_model(Status, request.args) return get_generic_model(models.Status, request.args)
@bp.route('/status', methods=['POST']) @bp.route('/status', methods=['POST'])
@jwt_required @jwt_required
@jwt_groups_accepted('admin', 'create') @jwt_groups_accepted('admin', 'create')
def create_status(): def create_status():
return create_generic_model(Status) return create_generic_model(models.Status)
@bp.route('/networks')
@jwt_required
def get_networks():
# TODO: add pagination
query = utils.get_query(Network.query, request.args)
networks = query.order_by(Network.address)
data = [network.to_dict() for network in networks]
return jsonify(data)
@bp.route('/networks', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin')
def create_network():
"""Create a new network"""
return create_generic_model(Network, mandatory_fields=(
'vlan_name', 'vlan_id', 'address', 'first_ip', 'last_ip', 'scope'))
@bp.route('/interfaces')
@jwt_required
def get_interfaces():
# TODO: add pagination
query = utils.get_query(Interface.query, request.args)
interfaces = query.order_by(Interface.ip)
data = [interface.to_dict() for interface in interfaces]
return jsonify(data)
@bp.route('/interfaces', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_interface():
"""Create a new interface"""
return create_generic_model(Interface, mandatory_fields=('network', 'ip', 'name'))
# -*- coding: utf-8 -*-
"""
app.api.network
~~~~~~~~~~~~~~~
This module implements the network API.
:copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.
"""
from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from .. import utils, models
from ..decorators import jwt_groups_accepted
from .utils import create_generic_model
bp = Blueprint('network_api', __name__)
@bp.route('/networks')
@jwt_required
def get_networks():
# TODO: add pagination
query = utils.get_query(models.Network.query, request.args)
networks = query.order_by(models.Network.address)
data = [network.to_dict() for network in networks]
return jsonify(data)
@bp.route('/networks', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin')
def create_network():
"""Create a new network"""
return create_generic_model(models.Network, mandatory_fields=(
'vlan_name', 'vlan_id', 'address', 'first_ip', 'last_ip', 'scope'))
@bp.route('/interfaces')
@jwt_required
def get_interfaces():
# TODO: add pagination
query = utils.get_query(models.Interface.query, request.args)
interfaces = query.order_by(models.Interface.ip)
data = [interface.to_dict() for interface in interfaces]
return jsonify(data)
@bp.route('/interfaces', methods=['POST'])
@jwt_required
@jwt_groups_accepted('admin', 'create')
def create_interface():
"""Create a new interface"""
return create_generic_model(models.Interface, mandatory_fields=('network', 'ip', 'name'))
# -*- coding: utf-8 -*-
"""
app.api.users
~~~~~~~~~~~~~
This module implements the users API.
:copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.
"""
from flask import current_app, Blueprint, jsonify, request
from flask_ldap3_login import AuthenticationResponseStatus
from ..extensions import ldap_manager
from .. import utils, tokens
bp = Blueprint('users_api', __name__)
@bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
if data is None:
raise utils.CSEntryError('Body should be a JSON object')
try:
username = data['username']
password = data['password']
except KeyError:
raise utils.CSEntryError('Missing mandatory field (username or password)', status_code=422)
response = ldap_manager.authenticate(username, password)
if response.status == AuthenticationResponseStatus.success:
current_app.logger.debug(f'{username} successfully logged in')
user = ldap_manager._save_user(
response.user_dn,
response.user_id,
response.user_info,
response.user_groups)
payload = {'access_token': tokens.generate_access_token(identity=user.id)}
return jsonify(payload), 200
raise utils.CSEntryError('Invalid credentials', status_code=401)
# -*- coding: utf-8 -*-
"""
app.api.utils
~~~~~~~~~~~~~
This module implements useful functions for the API.
:copyright: (c) 2017 European Spallation Source ERIC
:license: BSD 2-Clause, see LICENSE for more details.
"""
import sqlalchemy as sa
from flask import current_app, jsonify, request
from ..extensions import db
from .. import utils
def commit():
try:
db.session.commit()
except (sa.exc.IntegrityError, sa.exc.DataError) as e:
db.session.rollback()
raise utils.CSEntryError(str(e), status_code=422)
def get_generic_model(model, args):
"""Return data from model as json
:param model: model class
:param MultiDict args: args from the request
:returns: data from model as json
"""
items = model.query.order_by(model.name)
qrcode = args.get('qrcode', 'false').lower() == 'true'
data = [item.to_dict(qrcode=qrcode) for item in items]
return jsonify(data)
def create_generic_model(model, mandatory_fields=('name',)):
data = request.get_json()
if data is None:
raise utils.CSEntryError('Body should be a JSON object')
current_app.logger.debug(f'Received: {data}')
for mandatory_field in mandatory_fields:
if mandatory_field not in data:
raise utils.CSEntryError(f"Missing mandatory field '{mandatory_field}'", status_code=422)
try:
instance = model(**data)
except TypeError as e:
message = str(e).replace('__init__() got an ', '')
raise utils.CSEntryError(message, status_code=422)
except ValueError as e:
raise utils.CSEntryError(str(e), status_code=422)
db.session.add(instance)
commit()
return jsonify(instance.to_dict()), 201
...@@ -21,7 +21,9 @@ from .main.views import bp as main ...@@ -21,7 +21,9 @@ from .main.views import bp as main
from .inventory.views import bp as inventory from .inventory.views import bp as inventory
from .network.views import bp as network from .network.views import bp as network
from .users.views import bp as users from .users.views import bp as users
from .api.main import bp as api from .api.users import bp as users_api
from .api.inventory import bp as inventory_api
from .api.network import bp as network_api
from .defaults import defaults from .defaults import defaults
...@@ -121,7 +123,9 @@ def create_app(config=None): ...@@ -121,7 +123,9 @@ def create_app(config=None):
app.register_blueprint(inventory, url_prefix='/inventory') app.register_blueprint(inventory, url_prefix='/inventory')
app.register_blueprint(network, url_prefix='/network') app.register_blueprint(network, url_prefix='/network')
app.register_blueprint(users, url_prefix='/users') app.register_blueprint(users, url_prefix='/users')
app.register_blueprint(api, url_prefix='/api') app.register_blueprint(users_api, url_prefix='/api/v1/users')
app.register_blueprint(inventory_api, url_prefix='/api/v1/inventory')
app.register_blueprint(network_api, url_prefix='/api/v1/network')
app.wsgi_app = WhiteNoise(app.wsgi_app, root='static/') app.wsgi_app = WhiteNoise(app.wsgi_app, root='static/')
app.wsgi_app.add_files( app.wsgi_app.add_files(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment