diff --git a/addons/rest_api/__init__.py b/addons/rest_api/__init__.py new file mode 100755 index 00000000..7df6cc9f --- /dev/null +++ b/addons/rest_api/__init__.py @@ -0,0 +1,5 @@ +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models +from . import rest_exception diff --git a/addons/rest_api/__manifest__.py b/addons/rest_api/__manifest__.py new file mode 100755 index 00000000..69804662 --- /dev/null +++ b/addons/rest_api/__manifest__.py @@ -0,0 +1,28 @@ +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'REST API For Flectra', + 'version': '1.0.0', + 'category': 'API', + 'author': 'FlectraHQ', + 'website': 'https://www.flectrahq.com', + 'summary': 'REST API For Flectra', + 'description': """ +REST API For Flectra +==================== +With use of this module user can enable REST API in any Flectra applications/modules + +For detailed example of REST API refer *readme.md* +""", + 'depends': [ + 'web', + ], + 'data': [ + 'data/ir_configparameter_data.xml', + 'views/ir_model_view.xml', + 'views/res_user_view.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/rest_api/controllers/__init__.py b/addons/rest_api/controllers/__init__.py new file mode 100755 index 00000000..6bc474f3 --- /dev/null +++ b/addons/rest_api/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/rest_api/controllers/main.py b/addons/rest_api/controllers/main.py new file mode 100755 index 00000000..8751ac4d --- /dev/null +++ b/addons/rest_api/controllers/main.py @@ -0,0 +1,339 @@ +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +import functools +import hashlib +import os +import werkzeug.wrappers +import ast +try: + import simplejson as json +except ImportError: + import json +import flectra +from flectra import http +from flectra.http import request +from flectra import fields +from ..rest_exception import * + +_logger = logging.getLogger(__name__) + + +def eval_json_to_data(modelname, json_data, create=True): + Model = request.env[modelname] + model_fiels = Model._fields + field_name = [name for name, field in Model._fields.items()] + values = {} + for field in json_data: + if field not in field_name: + continue + if field not in field_name: + continue + val = json_data[field] + if not isinstance(val, list): + values[field] = val + else: + values[field] = [] + if not create and isinstance(model_fiels[field], fields.Many2many): + values[field].append((5,)) + for res in val: + recored = {} + for f in res: + recored[f] = res[f] + if isinstance(model_fiels[field], fields.Many2many): + values[field].append((4, recored['id'])) + + elif isinstance(model_fiels[field], flectra.fields.One2many): + if create: + values[field].append((0, 0, recored)) + else: + if 'id' in recored: + id = recored['id'] + del recored['id'] + values[field].append((1, id, recored)) if len(recored) else values[field].append((2, id)) + else: + values[field].append((0, 0, recored)) + return values + + +def object_read(modelname, default_domain, status_code, + post={}): + json_data = post + domain = default_domain or [] + field = [] + offset = 0 + limit = None + order = None + if 'filters' in json_data: + domain += ast.literal_eval(json_data['filters']) + elif 'field' in json_data: + field += ast.literal_eval(json_data['field']) + elif 'offset' in json_data: + offset = int(json_data['offset']) + elif 'limit' in json_data: + limit = int(json_data['limit']) + elif 'order' in json_data: + order = json_data['order'] + else: + pass + # Search Read object: + data = request.env[modelname].search_read(domain, offset=offset, limit=limit, order=order) + return valid_response(status=status_code, + data={ + 'count': len(data), 'results': data + }) + + +def object_read_one(modelname, id, status_code): + try: + id = int(id) + except: + pass + + if not id: + return invalid_object_id() + data = request.env[modelname].search_read(domain=[('id', '=', id)]) + if data: + return valid_response(status_code, data) + else: + return object_not_found() + + +def object_create_one(modelname, data, status_code): + rdata = request.httprequest.stream.read().decode('utf-8') + json_data = json.loads(rdata) + vals = eval_json_to_data(modelname, json_data) + if data: + vals.update(data) + try: + res = request.env[modelname].create(vals) + flectra_error = '' + except Exception as e: + res = None + flectra_error = e + if res: + return valid_response(status_code, {'id': res.id}) + else: + return no_object_created(flectra_error) + + +def object_update_one(modelname, id, status_code): + try: + id = int(id) + except: + id = None + if not id: + return invalid_object_id() + rdata = request.httprequest.stream.read().decode('utf-8') + json_data = ast.literal_eval(rdata) + vals = eval_json_to_data(modelname, json_data, create=False) + try: + res = request.env[modelname].browse(id).write(vals) + flectra_error = '' + except Exception as e: + res = None + flectra_error = e + if res: + return valid_response(status_code, 'Record Updated ' + 'successfully!') + else: + return no_object_updated(flectra_error) + + +def object_delete_one(modelname, id, status_code): + try: + id = int(id) + except: + id = None + if not id: + return invalid_object_id() + try: + res = request.env[modelname].browse(id).unlink() + flectra_error = '' + except Exception as e: + res = None + flectra_error = e + if res: + return valid_response(status_code, 'Record Successfully ' + 'Deleted!') + else: + return no_object_deleted(flectra_error) + + +def check_valid_token(func): + @functools.wraps(func) + def wrap(self, *args, **kwargs): + access_token = request.httprequest.headers.get('access_token') + if not access_token: + info = "Missing access token in request header!" + error = 'access_token_not_found' + _logger.error(info) + return invalid_response(400, error, info) + + access_token_data = request.env['oauth.access_token'].sudo().search( + [('token', '=', access_token)], order='id DESC', limit=1) + + if access_token_data._get_access_token(user_id=access_token_data.user_id.id) != access_token: + return invalid_token() + + request.session.uid = access_token_data.user_id.id + request.uid = access_token_data.user_id.id + return func(self, *args, **kwargs) + + return wrap + + +def generate_token(length=40): + random_data = os.urandom(100) + hash_gen = hashlib.new('sha512') + hash_gen.update(random_data) + return hash_gen.hexdigest()[:length] + + +# Read OAuth2 constants and setup token store: +db_name = flectra.tools.config.get('db_name') +if not db_name: + _logger.warning("Warning: To proper setup OAuth - it's necessary to " + "set the parameter 'db_name' in flectra config file!") + + +# List of REST resources in current file: +# (url prefix) (method) (action) +# /api/auth/get_tokens POST - Login in flectra and get access tokens +# /api/auth/delete_tokens POST - Delete access tokens from token store + + +# HTTP controller of REST resources: + +class ControllerREST(http.Controller): + + # Login in flectra database and get access tokens: + @http.route('/api/auth/get_tokens', methods=['POST'], type='http', + auth='none', csrf=False) + def api_auth_gettokens(self, **post): + # Convert http data into json: + db = post['db'] if post.get('db') else None + username = post['username'] if post.get('username') else None + password = post['password'] if post.get('password') else None + # Compare dbname (from HTTP-request vs. flectra config): + if db and (db != db_name): + info = "Wrong 'dbname'!" + error = 'wrong_dbname' + _logger.error(info) + return invalid_response(400, error, info) + + # Empty 'db' or 'username' or 'password: + if not db or not username or not password: + info = "Empty value of 'db' or 'username' or 'password'!" + error = 'empty_db_or_username_or_password' + _logger.error(info) + return invalid_response(400, error, info) + # Login in flectra database: + try: + request.session.authenticate(db, username, password) + except: + # Invalid database: + info = "Invalid database!" + error = 'invalid_database' + _logger.error(info) + return invalid_response(400, error, info) + + uid = request.session.uid + # flectra login failed: + if not uid: + info = "flectra User authentication failed!" + error = 'flectra_user_authentication_failed' + _logger.error(info) + return invalid_response(401, error, info) + + # Generate tokens + access_token = request.env['oauth.access_token']._get_access_token(user_id = uid, create = True) + + # Save all tokens in store + _logger.info("Save OAuth2 tokens of user in store...") + + # Successful response: + return werkzeug.wrappers.Response( + status=200, + content_type='application/json; charset=utf-8', + headers=[('Cache-Control', 'no-store'), + ('Pragma', 'no-cache')], + response=json.dumps({ + 'uid': uid, + 'user_context': request.session.get_context() if uid else {}, + 'company_id': request.env.user.company_id.id if uid else 'null', + 'access_token': access_token, + 'expires_in': request.env.ref('rest_api.oauth2_access_token_expires_in').sudo().value, + }), + ) + + # Delete access tokens from token store: + @http.route('/api/auth/delete_tokens', methods=['POST'], type='http', + auth='none', csrf=False) + def api_auth_deletetokens(self, **post): + # Try convert http data into json: + access_token = request.httprequest.headers.get('access_token') + access_token_data = request.env['oauth.access_token'].sudo().search( + [('token', '=', access_token)], order='id DESC', limit=1) + + if not access_token_data: + info = "No access token was provided in request!" + error = 'no_access_token' + _logger.error(info) + return invalid_response(400, error, info) + access_token_data.sudo().unlink() + # Successful response: + return valid_response( + 200, + {} + ) + + @http.route([ + '/api/', + '/api//' + ], type='http', auth="none", methods=['POST', 'GET', 'PUT', 'DELETE'], + csrf=False) + @check_valid_token + def restapi_access_token(self, model_name=False, id=False, **post): + Model = request.env['ir.model'] + Model_ids = Model.sudo().search([('model', '=', model_name), + ('rest_api', '=', True)]) + if Model_ids: + return getattr(self, '%s_data' % ( + request.httprequest.method).lower())( + model_name=model_name, id=id, **post) + return object_not_found() + + def get_data(self, model_name=False, id=False, **post): + if id: + return object_read_one( + modelname=model_name, + id=id, + status_code=200, + ) + return object_read( + modelname=model_name, + default_domain=[], + status_code=200, + post=post + ) + + def put_data(self, model_name=False, id=False, **post): + return object_update_one( + modelname=model_name, + id=id, + status_code=200, + ) + + def post_data(self, model_name=False, id=False, **post): + return object_create_one( + modelname=model_name, + data={}, + status_code=200, + ) + + def delete_data(self, model_name=False, id=False): + return object_delete_one( + modelname=model_name, + id=id, + status_code=200 + ) diff --git a/addons/rest_api/data/ir_configparameter_data.xml b/addons/rest_api/data/ir_configparameter_data.xml new file mode 100755 index 00000000..37588cd1 --- /dev/null +++ b/addons/rest_api/data/ir_configparameter_data.xml @@ -0,0 +1,11 @@ + + + + + + + oauth2_access_token_expires_in + 600 + + + diff --git a/addons/rest_api/models/__init__.py b/addons/rest_api/models/__init__.py new file mode 100644 index 00000000..12d3057c --- /dev/null +++ b/addons/rest_api/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from .import ir_model +from .import oauth_provider diff --git a/addons/rest_api/models/ir_model.py b/addons/rest_api/models/ir_model.py new file mode 100644 index 00000000..07a0d56b --- /dev/null +++ b/addons/rest_api/models/ir_model.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +from flectra import api, fields, models, modules, _ + + +class IrModel(models.Model): + _inherit = 'ir.model' + + rest_api = fields.Boolean('REST API', default=True, + help="Enable REST API for this object/model") diff --git a/addons/rest_api/models/oauth_provider.py b/addons/rest_api/models/oauth_provider.py new file mode 100644 index 00000000..e6dd9524 --- /dev/null +++ b/addons/rest_api/models/oauth_provider.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Author: Ivan Yelizariev, Ildar +# Ref. from: https://github.com/it-projects-llc/odoo-saas-tools/blob/10.0/oauth_provider/models/oauth_provider.py + +from flectra import models, fields, api +from datetime import datetime, timedelta +from flectra.tools import DEFAULT_SERVER_DATETIME_FORMAT + +try: + from oauthlib import common as oauthlib_common +except: + pass + + +class OauthAccessToken(models.Model): + _name = 'oauth.access_token' + + token = fields.Char('Access Token', required=True) + user_id = fields.Many2one('res.users', string='User', required=True) + expires = fields.Datetime('Expires', required=True) + scope = fields.Char('Scope') + + @api.multi + def _get_access_token(self, user_id=None, create=False): + if not user_id: + user_id = self.env.user.id + + access_token = self.env['oauth.access_token'].sudo().search( + [('user_id', '=', user_id)], order='id DESC', limit=1) + if access_token: + access_token = access_token[0] + if access_token.is_expired(): + access_token = None + if not access_token and create: + expires = datetime.now() + timedelta(seconds=int(self.env.ref('rest_api.oauth2_access_token_expires_in').sudo().value)) + vals = { + 'user_id': user_id, + 'scope': 'userinfo', + 'expires': expires.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'token': oauthlib_common.generate_token(), + } + access_token = self.env['oauth.access_token'].sudo().create(vals) + # we have to commit now, because /oauth2/tokeninfo could + # be called before we finish current transaction. + self._cr.commit() + if not access_token: + return None + return access_token.token + + @api.multi + def is_valid(self, scopes=None): + """ + Checks if the access token is valid. + + :param scopes: An iterable containing the scopes to check or None + """ + self.ensure_one() + return not self.is_expired() and self._allow_scopes(scopes) + + @api.multi + def is_expired(self): + self.ensure_one() + return datetime.now() > fields.Datetime.from_string(self.expires) + + @api.multi + def _allow_scopes(self, scopes): + self.ensure_one() + if not scopes: + return True + + provided_scopes = set(self.scope.split()) + resource_scopes = set(scopes) + + return resource_scopes.issubset(provided_scopes) + + +class Users(models.Model): + _inherit = 'res.users' + token_ids = fields.One2many('oauth.access_token', 'user_id', + string="Access Tokens") \ No newline at end of file diff --git a/addons/rest_api/readme.md b/addons/rest_api/readme.md new file mode 100644 index 00000000..cf4559a9 --- /dev/null +++ b/addons/rest_api/readme.md @@ -0,0 +1,60 @@ +# REST API For Flectra + +This module enable REST API in any Flectra applications/modules. + + +## How to Use + + +```python +import requests, json, pprint + +data = {'username': 'admin', 'password': 'admin', 'db': 'db_flectra_base'} +s = requests.post('http://localhost:7073/api/auth/get_tokens', data=data) +content = json.loads(s.content.decode('utf-8')) +headers = {'access_token': content.get('access_token')} +``` + +**GET request** +```python +p = requests.get('http://localhost:7073/api/res.partner/', headers=headers, + data=json.dumps({'limit': 2})) +# ***Pass optional parameter like this*** +# { +# 'limit': 10, 'filters': "[('supplier','=',True),('parent_id','=', False)]", +# 'order': 'name asc', 'offset': 10 +# } +print(p.content) +``` + +**POST request** +```python +p = requests.post('http://localhost:7073/api/res.partner/', headers=headers, + data=json.dumps({ + 'name':'John', + 'country_id': 105, + 'child_ids': [{'name': 'Contact', 'type':'contact'}, + {'name': 'Invoice', 'type':'invoice'}], + 'category_id': [{'id':9}, {'id': 10}] + } +)) +print(p.content) +``` + +**PUT Request** +```python +p = requests.put('http://localhost:7073/api/res.partner/68', headers=headers, + data=json.dumps({ + 'name':'John Doe', + 'country_id': 107, + 'category_id': [{'id': 10}] + } +)) +print(p.content) +``` + +**DELETE Request** +```python +p = requests.delete('http://localhost:7073/api/res.partner/68', headers=headers) +print(p.content) +``` diff --git a/addons/rest_api/rest_exception.py b/addons/rest_api/rest_exception.py new file mode 100644 index 00000000..ddb0b0c0 --- /dev/null +++ b/addons/rest_api/rest_exception.py @@ -0,0 +1,73 @@ +# Part of Flectra. See LICENSE file for full copyright and licensing details. + +import logging +import werkzeug.wrappers + +try: + import simplejson as json +except ImportError: + import json + +_logger = logging.getLogger(__name__) + + +def valid_response(status, data): + return werkzeug.wrappers.Response( + status=status, + content_type='application/json; charset=utf-8', + response=json.dumps(data), + ) + + +def invalid_response(status, error, info): + return werkzeug.wrappers.Response( + status=status, + content_type='application/json; charset=utf-8', + response=json.dumps({ + 'error': error, + 'error_descrip': info, + }), + ) + + +def invalid_object_id(): + _logger.error("Invalid object 'id'!") + return invalid_response(400, 'invalid_object_id', "Invalid object 'id'!") + + +def invalid_token(): + _logger.error("Token is expired or invalid!") + return invalid_response(401, 'invalid_token', "Token is expired or invalid!") + + +def object_not_found(): + _logger.error("Not found object(s) in flectra!") + return invalid_response(404, 'not_found_object_in_flectra', + "Not found object(s) in flectra!") + + +def unable_delete(): + _logger.error("Access Denied!") + return invalid_response(404, "you don't have access to delete records for " + "this model", "Access Denied!") + + +def no_object_created(flectra_error): + _logger.error("Not created object in flectra! ERROR: %s" % flectra_error) + return invalid_response(409, 'not_created_object_in_flectra', + "Not created object in flectra! ERROR: %s" % + flectra_error) + + +def no_object_updated(flectra_error): + _logger.error("Not updated object in flectra! ERROR: %s" % flectra_error) + return invalid_response(409, 'not_updated_object_in_flectra', + "Not updated object in flectra! ERROR: %s" % + flectra_error) + + +def no_object_deleted(flectra_error): + _logger.error("Not deleted object in flectra! ERROR: %s" % flectra_error) + return invalid_response(409, 'not_deleted_object_in_flectra', + "Not deleted object in flectra! ERROR: %s" % + flectra_error) diff --git a/addons/rest_api/security/ir.model.access.csv b/addons/rest_api/security/ir.model.access.csv new file mode 100644 index 00000000..edbd13d6 --- /dev/null +++ b/addons/rest_api/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_oauth_access_token,access_oauth_access_token,model_oauth_access_token,,1,1,1,1 diff --git a/addons/rest_api/views/ir_model_view.xml b/addons/rest_api/views/ir_model_view.xml new file mode 100644 index 00000000..c6e32a95 --- /dev/null +++ b/addons/rest_api/views/ir_model_view.xml @@ -0,0 +1,23 @@ + + + + + ir.model + + + + + + + + + + ir.model + + + + + + + + \ No newline at end of file diff --git a/addons/rest_api/views/res_user_view.xml b/addons/rest_api/views/res_user_view.xml new file mode 100644 index 00000000..48926cd7 --- /dev/null +++ b/addons/rest_api/views/res_user_view.xml @@ -0,0 +1,17 @@ + + + + + res.users + + + + + + + + + + + + \ No newline at end of file