diff --git a/app/config/development.default.py b/app/config/development.default.py index 18a4f9e357d3aae0464a011adc3b86e4aa10d3f8..216af3cf14582f8b8a84e6f4c8e209b159b5f315 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -16,7 +16,8 @@ SERVICE_CONFIG = { 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://dev.appservice.blocpower.io/', + 'app': 'http://dev.appservice.blocpower.io', + 'user': 'http://dev.userservice.blocpower.io', } } diff --git a/app/config/local.default.py b/app/config/local.default.py index 41819cb12b9e78ec10c509551927fc69f212b30c..a53dfc0f07ff43889c14582cfe3b031339e4e848 100644 --- a/app/config/local.default.py +++ b/app/config/local.default.py @@ -14,6 +14,7 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://127.0.0.1:5400', + 'user': 'http://127.0.0.1:5401', } } diff --git a/app/config/production.default.py b/app/config/production.default.py index 74fbbb2691fb38d49bf4a7fe637953885ae875c8..d03bb1f743fc6fabc512937872ae1193368356da 100644 --- a/app/config/production.default.py +++ b/app/config/production.default.py @@ -17,6 +17,7 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://app.s.blocpower.us', + 'user': 'http://user.s.blocpower.us', } } diff --git a/app/config/staging.default.py b/app/config/staging.default.py index 1e82cec2bd5b6b39c71a85ef0eebfe297f745da4..36a1f6d2a31e39802a80061dfaf5bd78c5c82100 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -16,7 +16,8 @@ SERVICE_CONFIG = { 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://staging.app.s.blocpower.us/', + 'app': 'http://staging.app.s.blocpower.us', + 'user': 'http://staging.user.s.blocpower.us.' } } diff --git a/app/permissions/auth.py b/app/permissions/auth.py index de953107ad8d8e983a530cff360bdc6a9d95128c..b7be02c75c6bf4f061af038295c4095f8e2a41bc 100644 --- a/app/permissions/auth.py +++ b/app/permissions/auth.py @@ -1,12 +1,9 @@ """Permissions to check authentication.""" +import json +import requests from flask import current_app, g from werkzeug.exceptions import Unauthorized from jose import jwt -import json -import requests - -from ..lib.red import redis -from ..lib.service import services from .base import Permission from .application import app_need @@ -14,8 +11,7 @@ from .application import app_need class AuthNeed(Permission): """Check if the access token is valid.""" - def __init__(self, required_permissions=[]): - self.required_permissions = required_permissions + def __init__(self): super().__init__() def is_met(self): @@ -27,6 +23,7 @@ class AuthNeed(Permission): AUTH0_DOMAIN = current_app.config['AUTH0_DOMAIN'] API_AUDIENCE = current_app.config['AUTH0_AUDIENCE'] ALGORITHMS = ["RS256"] + r = requests.get("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json") jwks = json.loads(r.text) unverified_header = jwt.get_unverified_header(auth0_token) @@ -50,8 +47,7 @@ class AuthNeed(Permission): issuer="https://"+AUTH0_DOMAIN+"/" ) g.sub = payload['sub'] - # For now we will print and return unauthorized. In the future - # we will log these errors and the requester + except jwt.ExpiredSignatureError: current_app.logger.info('Token is expired') return False @@ -62,22 +58,8 @@ class AuthNeed(Permission): current_app.logger.info('Invalid header. Unable to parse the token') return False - # Check permissions - # The self.bool_ variable is a boolean if no value is passed in - if self.required_permissions: - CLAIMS_NAMESPACE = current_app.config['AUTH0_CLAIMS_NAMESPACE'] - permission_key = '{}permissions'.format(CLAIMS_NAMESPACE) - if permission_key not in payload: - return False - actual_permissions = payload[permission_key] - for permission in self.required_permissions: - if permission not in actual_permissions: - return False return True return False auth_need = AuthNeed() standard_login_need = app_need & auth_need -# An example permission need decorator. This decorator requires that the user -# has the 'view::developer' permission -developer_view_need = app_need & AuthNeed(['view::developer']) diff --git a/app/permissions/authorization.py b/app/permissions/authorization.py new file mode 100644 index 0000000000000000000000000000000000000000..fcb5a1c706731ad4d587a2912edc202a141c0e43 --- /dev/null +++ b/app/permissions/authorization.py @@ -0,0 +1,47 @@ +"""Authorization decorator""" +from functools import wraps +from flask import current_app, g, request +from werkzeug.exceptions import Unauthorized +from ..lib.service import services + +CRUD_TO_REST = { + 'POST': 'create', + 'GET': 'read', + 'PUT': 'update', # TODO: if no id 'create' + 'PATCH': 'update', + 'DELETE': 'delete', +} + + +def secured(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if g.sub is not None: + current_app.logger.info('{} accessing {}'.format(g.sub, request.endpoint)) + + action = CRUD_TO_REST[request.method] + resource = request.endpoint.split(':').pop(0) + if resource.endswith('View'): + resource = resource[:-4] + + from flask import session + auth0_header = current_app.config.get('AUTH0_AUTH_HEADER') + headers = {} + headers[auth0_header] = request.headers.get(auth0_header) + params = {'permissions': 'true'} + + response = services.user.get('/user/{}'.format(g.sub), params=params, headers=headers) + if not response.status_code == 200: + raise Unauthorized + + data = response.json() + + permissions = data['data'][g.sub]['permissions'] + action_resource = '{action}::{resource}'.format(action=action, resource=resource) + if action_resource not in permissions: + raise Unauthorized + + else: + raise Unauthorized + return f(*args, **kwargs) + return decorated_function diff --git a/app/views/base.py b/app/views/base.py index 4700da8cccec2b0ca3e7b047f29277ac4361303d..bf038245d77c430e118f33ad6f0197f95a839728 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -1,8 +1,9 @@ from bpvalve.flask.views import UnprotectedRestView from app.permissions.auth import standard_login_need +from ..permissions.authorization import secured class RestView(UnprotectedRestView): """A view wrapper for RESTful controllers that _does_ offer API protection.""" - decorators = (standard_login_need,) + decorators = (secured, standard_login_need) diff --git a/requirements.txt b/requirements.txt index 8fce59b1edca82b89e9bf0de9d98a9c752bd79d2..c8240636794e0261b64b8db173eebcb6d4d24cb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ arrow==0.7.0 blessed==1.9.5 -botocore==1.3.28 -git+ssh://git@github.com/Blocp/bpvalve.git@v1.1.2 +botocore==1.5.48 +git+ssh://git@github.com/Blocp/bpvalve.git@v1.2.0 cement==2.4.0 colorama==0.3.3 docker-py==1.1.0