diff --git a/README.md b/README.md index 844ab24..a79b7ff 100644 --- a/README.md +++ b/README.md @@ -16,16 +16,23 @@ pip install -e git+https://github.com/GeographicaGS/Longitude#egg=longitude You need to be part of Geographica's developer team to be able to accomplish this task. + Start docker ``` docker-compose run --rm python bash ``` + +Install needed dependencies +``` +pip install -r requirements.txt +``` + Install twine ``` pip install twine ``` -Set vertion at setup.py +Set version at setup.py Upload: ``` diff --git a/longitude/auth.py b/longitude/auth.py index 4c0f602..0e915f9 100644 --- a/longitude/auth.py +++ b/longitude/auth.py @@ -7,7 +7,9 @@ from functools import wraps from flask import Blueprint, jsonify, request -from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity +from flask_jwt_extended import ( + JWTManager, jwt_required, create_access_token, get_jwt_identity +) from .config import cfg from .models.user_model import UserModel from dateutil.parser import parse @@ -21,7 +23,6 @@ def ini(app): log.info(cfg['AUTH_TOKEN_EXPIRATION']) - JWTManager(app) app.config['JWT_TOKEN_LOCATION'] = ['headers', 'query_string'] app.config['JWT_ACCESS_TOKEN_EXPIRES'] = EXPIRATION_DELTA @@ -35,9 +36,23 @@ def ini(app): routes = Blueprint('auth', __name__) -def auth(): - """ - AUTH decorator. It checks for a valid token and validates this token against the DB +def auth(allow_profiles=None, disallow_profiles=None): + """AUTH decorator. + + It checks for a valid token and validates this token against the DB. + + With AUTH_USE_PROFILES env var activated it will aditionally check if + the user has a allowed or disallowed profile for the endpoint. + You can only specify a set of allowed profiles or a set of disallowed + profiles. + + :param allow_profiles: A list of profiles/roles that can access \ + the endpoint, defaults to None. + :param allow_profiles: list[str], optional. + :param disallow_profiles: A list of profiles/roles that cannot access \ + the endpoint, defaults to None + :param disallow_profiles: list[str], optional. + :raises ValueError: If both allowed and disallowed profiles are specified. """ def decorator(func): @@ -58,7 +73,9 @@ def check_token(*args, **kwargs): token = request.args.get('jwt') if not token: - return jsonify({'msg': 'You must provide an authorization header (token)'}), 401 + return jsonify({ + 'msg': 'You must provide an authorization header (token)' + }), 401 if cfg['AUTH_TOKEN_DOBLE_CHECK']: user_model = UserModel({ @@ -70,6 +87,29 @@ def check_token(*args, **kwargs): if not valid: return jsonify({'msg': 'No valid token'}), 403 + if cfg['AUTH_USE_PROFILES']: + allow_type_prfs = check_auth_profiles(allow_profiles) + disallow_type_prfs = check_auth_profiles(disallow_profiles) + user_prof = user_data['profile'] + + err_response = jsonify( + { + 'msg': 'You are not authorized to request ' + 'this information' + } + ), 403 + + if (allow_type_prfs is list and + disallow_type_prfs is list): + raise ValueError('You can only specify allowed profiles ' + 'or disallowed profiles') + elif allow_type_prfs is list: + if user_prof not in allow_profiles: + return err_response + elif disallow_type_prfs is list: + if user_prof in disallow_profiles: + return err_response + request.user = user_data request.token = token @@ -79,10 +119,35 @@ def check_token(*args, **kwargs): return decorator + def check_login_fields(fields_list, user_data, username): check_list = [True for x in fields_list if user_data[x] == username] return True if check_list else False + +def check_auth_profiles(profiles): + correct = True + prof_type = None + + if type(profiles) is list: + prof_type = list + if len(profiles): + for element in profiles: + if type(element) is not str: + correct = False + break + prof_type = list if correct else None + elif profiles is None: + pass + else: + correct = False + + if not correct: + raise ValueError('Profiles parameter is expected to be a list[str]') + else: + return prof_type + + @routes.route('/token', methods=['GET']) def get_token(): """ @@ -104,8 +169,10 @@ def get_token(): login_fields = cfg['AUTH_LOGIN_FIELDS'].split(',') - if not user_data or not check_login_fields(login_fields, user_data, username) or not bcrypt.checkpw(password.encode('utf8'), - user_data['password'].encode('utf-8')): + if (not user_data or + not check_login_fields(login_fields, user_data, username) or + not bcrypt.checkpw(password.encode('utf8'), + user_data['password'].encode('utf-8'))): return jsonify({'msg': 'Bad username or password'}), 401 if cfg['AUTH_ACCOUNT_EXPIRATION_FIELD']: @@ -117,8 +184,11 @@ def get_token(): user_model.update_last_access(user_data['id']) if cfg['EXTRA_JWT_IDENTITY_FIELDS']: - extra_jwt_identity_fields = {key.replace('JWT_IDENTITY_', '').lower(): value for (key, value) in - cfg['EXTRA_JWT_IDENTITY_FIELDS'].items()} + extra_jwt_identity_fields = { + key.replace('JWT_IDENTITY_', '').lower(): value + for (key, value) in + cfg['EXTRA_JWT_IDENTITY_FIELDS'].items() + } user_data.update(extra_jwt_identity_fields) del user_data['password'] diff --git a/longitude/config.py b/longitude/config.py index 0ccfdaf..c67770d 100644 --- a/longitude/config.py +++ b/longitude/config.py @@ -23,7 +23,8 @@ 'AUTH_LAST_ACCESS_FIELD': os.environ.get('AUTH_LAST_ACCESS_FIELD', None), 'AUTH_ACCOUNT_EXPIRATION_FIELD': os.environ.get('AUTH_ACCOUNT_EXPIRATION_FIELD', None), 'AUTH_UPDATE_LAST_ACCESS': bool(int(os.environ.get('AUTH_UPDATE_LAST_ACCESS', 0))), - 'AUTH_HEADER_NAME': os.environ.get('AUTH_HEADER_NAME', 'Authorization') + 'AUTH_HEADER_NAME': os.environ.get('AUTH_HEADER_NAME', 'Authorization'), + 'AUTH_USE_PROFILES': bool(int(os.environ.get('AUTH_USE_PROFILES', 0))) } diff --git a/longitude/models/user/abstract_user_model.py b/longitude/models/user/abstract_user_model.py index b52f9a0..8f47d00 100644 --- a/longitude/models/user/abstract_user_model.py +++ b/longitude/models/user/abstract_user_model.py @@ -18,3 +18,7 @@ def check_user_token(self, token): @abstractmethod def delete_user_token(self, user_id): pass + + @abstractmethod + def update_last_access(self, user_id): + pass diff --git a/longitude/models/user/postgres_user_model.py b/longitude/models/user/postgres_user_model.py index 95524b5..78d4384 100644 --- a/longitude/models/user/postgres_user_model.py +++ b/longitude/models/user/postgres_user_model.py @@ -3,6 +3,7 @@ """ from longitude.models.base_models import PostgresModel from longitude.models.user import AbstractUserModel +from longitude.models.utils import SQLTrustedString class PostgresUserModel(AbstractUserModel, PostgresModel): diff --git a/requirements.txt b/requirements.txt index 8860229..d896975 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ certifi==2017.11.5 cffi==1.11.5 chardet==3.0.4 click==6.7 -Flask==0.12.2 +Flask==1.0.2 Flask-JWT-Extended==3.12.0 future==0.15.2 idna==2.6 @@ -18,7 +18,7 @@ PyJWT==1.6.1 pyrestcli==0.6.4 python-dateutil==2.5.3 redis==2.10.6 -requests==2.18.4 +requests==2.20.1 requests-toolbelt==0.8.0 six==1.11.0 tqdm==4.19.4 diff --git a/setup.py b/setup.py index 81794fd..c841e2f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup( name='geographica-longitude', - version='0.1.31', + version='0.2.0', description='Longitude', long_description=long_description,