From ce89dae273043ac181ece757eadf7a9a41f72080 Mon Sep 17 00:00:00 2001 From: Alessandro Date: Wed, 8 Nov 2017 15:06:23 -0500 Subject: [PATCH 1/3] Authorization (#39) * Add user authorization * Update bpvalve to v1.2.0 * Remove permissions from auth * Remove unused imports * Log user authoorization * Update botocore version * Clean up user service request --- app/config/development.default.py | 3 +- app/config/local.default.py | 1 + app/config/production.default.py | 1 + app/config/staging.default.py | 3 +- app/permissions/auth.py | 28 ++++-------------- app/permissions/authorization.py | 47 +++++++++++++++++++++++++++++++ app/views/base.py | 3 +- requirements.txt | 4 +-- 8 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 app/permissions/authorization.py diff --git a/app/config/development.default.py b/app/config/development.default.py index 18a4f9e..216af3c 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 41819cb..a53dfc0 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 74fbbb2..d03bb1f 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 1e82cec..36a1f6d 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 de95310..b7be02c 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 0000000..fcb5a1c --- /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 4700da8..bf03824 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 87be95d..c824063 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.1 +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 -- GitLab From 91a26793f547b8eb071e84b6694ede2a9eec3709 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Fri, 10 Nov 2017 10:38:33 -0500 Subject: [PATCH 2/3] Remove trailing dot in url --- app/config/staging.default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/staging.default.py b/app/config/staging.default.py index 36a1f6d..21bdbcb 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -17,7 +17,7 @@ SERVICE_CONFIG = { 'app_secret': 'x-blocpower-app-secret'}, 'urls': { 'app': 'http://staging.app.s.blocpower.us', - 'user': 'http://staging.user.s.blocpower.us.' + 'user': 'http://staging.user.s.blocpower.us' } } -- GitLab From 5d1a8147cee98d74e2157679230cae4b65419435 Mon Sep 17 00:00:00 2001 From: Alessandro DiMarco Date: Fri, 10 Nov 2017 11:41:55 -0500 Subject: [PATCH 3/3] Remove auth0 from configs --- app/config/development.default.py | 8 -------- app/config/production.default.py | 8 -------- app/config/staging.default.py | 8 -------- 3 files changed, 24 deletions(-) diff --git a/app/config/development.default.py b/app/config/development.default.py index 9f3058e..7b4be41 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -8,11 +8,3 @@ DEBUG = True HEADER_APP_KEY = 'x-blocpower-app-key' HEADER_APP_SECRET = 'x-blocpower-app-secret' - -# Auth0 Authentication -AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' -AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] -AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] -AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] -AUTH0_CLIENT_ID = os.environ['AUTH0_CLIENT_ID'] -AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] diff --git a/app/config/production.default.py b/app/config/production.default.py index 36d39fc..e576092 100644 --- a/app/config/production.default.py +++ b/app/config/production.default.py @@ -7,11 +7,3 @@ DEBUG = False HEADER_APP_KEY = 'x-blocpower-app-key' HEADER_APP_SECRET = 'x-blocpower-app-secret' - -# Auth0 Authentication -AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' -AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] -AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] -AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] -AUTH0_CLIENT_ID = os.environ['AUTH0_CLIENT_ID'] -AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] diff --git a/app/config/staging.default.py b/app/config/staging.default.py index 9f3058e..7b4be41 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -8,11 +8,3 @@ DEBUG = True HEADER_APP_KEY = 'x-blocpower-app-key' HEADER_APP_SECRET = 'x-blocpower-app-secret' - -# Auth0 Authentication -AUTH0_AUTH_HEADER = 'x-blocpower-auth0-token' -AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN'] -AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE'] -AUTH0_CLAIMS_NAMESPACE = os.environ['AUTH0_CLAIMS_NAMESPACE'] -AUTH0_CLIENT_ID = os.environ['AUTH0_CLIENT_ID'] -AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] -- GitLab