diff --git a/api/auth/security.py b/api/auth/security.py index 5380a55..33e88dc 100644 --- a/api/auth/security.py +++ b/api/auth/security.py @@ -6,12 +6,15 @@ from api.auth.cas_auth import validate_proxy, validate_bearer, validate_cas_request from api.maap_database import db from api.models.member import Member +from api.models.role import Role HEADER_PROXY_TICKET = "proxy-ticket" HEADER_CP_TICKET = "cpticket" HEADER_AUTHORIZATION = "Authorization" HEADER_CAS_AUTHORIZATION = "cas-authorization" HEADER_DPS_TOKEN = "dps-token" +MEMBER_STATUS_ACTIVE = "active" +MEMBER_STATUS_SUSPENDED = "suspended" def get_authorized_user(): @@ -34,34 +37,36 @@ def get_authorized_user(): return None -def login_required(wrapped_function): - @wraps(wrapped_function) - def wrap(*args, **kwargs): +def login_required(role=Role.ROLE_GUEST): + def login_required_outer(wrapped_function): + @wraps(wrapped_function) + def wrap(*args, **kwargs): + auth = get_auth_header() - auth = get_auth_header() + if auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET: + member_session = validate_proxy(request.headers[auth]) - if ((auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET) and - validate_proxy(request.headers[auth]) is not None): - return wrapped_function(*args, **kwargs) + if member_session is not None and member_session.member.role_id >= role: + return wrapped_function(*args, **kwargs) - if auth == HEADER_AUTHORIZATION: - bearer = request.headers.get(auth) - token = bearer.split()[1] - authorized = validate_bearer(token) + if auth == HEADER_AUTHORIZATION: + bearer = request.headers.get(auth) + token = bearer.split()[1] + authorized = validate_bearer(token) - if authorized is not None: - return wrapped_function(*args, **kwargs) - - if auth == HEADER_CAS_AUTHORIZATION and validate_cas_request(request.headers[auth]) is not None: - return wrapped_function(*args, **kwargs) + if authorized is not None: + return wrapped_function(*args, **kwargs) - if auth == HEADER_DPS_TOKEN and valid_dps_request(): - return wrapped_function(*args, **kwargs) + if auth == HEADER_CAS_AUTHORIZATION and validate_cas_request(request.headers[auth]) is not None: + return wrapped_function(*args, **kwargs) - abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") + if auth == HEADER_DPS_TOKEN and valid_dps_request(): + return wrapped_function(*args, **kwargs) - return wrap + abort(status.HTTP_403_FORBIDDEN, description="Not authorized.") + return wrap + return login_required_outer def valid_dps_request(): if HEADER_DPS_TOKEN in request.headers: diff --git a/api/endpoints/admin.py b/api/endpoints/admin.py index f732205..77afed6 100755 --- a/api/endpoints/admin.py +++ b/api/endpoints/admin.py @@ -2,6 +2,7 @@ from flask_restx import Resource from flask import request from flask_api import status +from api.models.role import Role from api.restplus import api from api.auth.security import login_required from api.maap_database import db @@ -10,16 +11,16 @@ from datetime import datetime import json -log = logging.getLogger(__name__) +from api.utils.http_util import err_response +log = logging.getLogger(__name__) ns = api.namespace('admin', description='Operations related to the MAAP admin') - @ns.route('/pre-approved') class PreApprovedEmails(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required(role=Role.ROLE_ADMIN) def get(self): pre_approved = db.session.query( PreApproved.email, @@ -31,7 +32,7 @@ def get(self): return result @api.doc(security='ApiKeyAuth') - @login_required + @login_required(role=Role.ROLE_ADMIN) def post(self): """ @@ -79,7 +80,7 @@ def post(self): class PreApprovedEmails(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required(role=Role.ROLE_ADMIN) def delete(self, email): """ Delete pre-approved email @@ -94,5 +95,3 @@ def delete(self, email): db.session.commit() return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(email)} - - diff --git a/api/endpoints/algorithm.py b/api/endpoints/algorithm.py index 92e98c6..9ec7110 100644 --- a/api/endpoints/algorithm.py +++ b/api/endpoints/algorithm.py @@ -3,9 +3,10 @@ from flask import request, Response from flask_restx import Resource, reqparse from flask_api import status + +from api.models.member import Member from api.restplus import api import re -import json import traceback import api.utils.github_util as git import api.utils.hysds_util as hysds @@ -91,7 +92,7 @@ class Register(Resource): ]""") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This will create the hysds spec files and commit to git @@ -422,7 +423,7 @@ def get(self, algo_id): .format(ex, tb)), status=status.HTTP_500_INTERNAL_SERVER_ERROR, mimetype='text/xml') @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def delete(self, algo_id): """ delete a registered algorithm @@ -494,7 +495,7 @@ def post(self): class Publish(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This endpoint is called by a logged-in user to make an algorithm public diff --git a/api/endpoints/job.py b/api/endpoints/job.py index bc93de8..0e7474b 100644 --- a/api/endpoints/job.py +++ b/api/endpoints/job.py @@ -30,7 +30,7 @@ class Submit(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): """ This will submit jobs to the Job Execution System (HySDS) @@ -468,7 +468,7 @@ class Jobs(Resource): parser.add_argument('username', required=False, type=str, help="Username of job submitter") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): """ Returns a list of jobs for a given user @@ -581,7 +581,7 @@ class JobsByUser(Resource): parser.add_argument('status', type=str, help="Job status, e.g. Accepted, Running, Succeeded, Failed, etc.", required=False) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, username): """ Returns a list of jobs for a given user @@ -619,7 +619,7 @@ class StopJobs(Resource): help="Wait for Cancel job to finish") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, job_id): # TODO: add optional parameter wait_for_completion to wait for cancel job to complete. # Since this can take a long time, we don't wait by default. diff --git a/api/endpoints/members.py b/api/endpoints/members.py index 8d35e6c..b1c576c 100755 --- a/api/endpoints/members.py +++ b/api/endpoints/members.py @@ -3,9 +3,11 @@ from flask_restx import Resource, reqparse from flask import request, jsonify, Response from flask_api import status +from sqlalchemy.exc import SQLAlchemyError from api.restplus import api import api.settings as settings -from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request +from api.auth.security import get_authorized_user, login_required, valid_dps_request, edl_federated_request, \ + MEMBER_STATUS_ACTIVE, MEMBER_STATUS_SUSPENDED from api.maap_database import db from api.utils import github_util from api.models.member import Member as Member_db @@ -22,6 +24,8 @@ import boto3 import requests from urllib import parse + +from api.utils.http_util import err_response, custom_response from api.utils.url_util import proxied_url from cryptography.fernet import Fernet @@ -31,26 +35,12 @@ sts_client = boto3.client('sts', region_name=settings.AWS_REGION) fernet = Fernet(settings.FERNET_KEY) -STATUS_ACTIVE = "active" -STATUS_SUSPENDED = "suspended" - - -def custom_response(msg, code=status.HTTP_200_OK): - return { - 'code': code, - 'message': msg - }, code - - -def err_response(msg, code=status.HTTP_400_BAD_REQUEST): - return custom_response(msg, code) - @ns.route('') class Member(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): members = db.session.query( Member_db.id, @@ -71,7 +61,7 @@ def get(self): class Member(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, key): cols = [ @@ -109,7 +99,7 @@ def get(self, key): pgt_result = json.loads(member_session_schema.dumps(pgt_ticket)) result = json.loads(json.dumps(dict(result.items() | pgt_result.items()))) - # If the requested user info belongs to the logged in user, + # If the requested user info belongs to the logged-in user, # also include additional ssh key information belonging to the user if member.username == key: cols = [ @@ -130,7 +120,7 @@ def get(self, key): return result @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, key): """ @@ -191,7 +181,7 @@ def post(self, key): (~PreApproved.email.like("*%") & PreApproved.email.like(email)) ).first() - status = STATUS_SUSPENDED if pre_approved_email is None else STATUS_ACTIVE + member_status = MEMBER_STATUS_SUSPENDED if pre_approved_email is None else MEMBER_STATUS_ACTIVE guest = Member_db(first_name=first_name, last_name=last_name, @@ -201,14 +191,14 @@ def post(self, key): public_ssh_key=req_data.get("public_ssh_key", None), public_ssh_key_modified_date=datetime.utcnow(), public_ssh_key_name=req_data.get("public_ssh_key_name", None), - status=status, + status=member_status, creation_date=datetime.utcnow()) db.session.add(guest) db.session.commit() # Send Email Notifications based on member status - if status == STATUS_ACTIVE: + if member_status == MEMBER_STATUS_ACTIVE: send_user_status_change_email(guest, True, True, proxied_url(request)) send_welcome_to_maap_active_user_email(guest, proxied_url(request)) else: @@ -219,7 +209,7 @@ def post(self, key): return json.loads(member_schema.dumps(guest)) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def put(self, key): """ @@ -283,7 +273,7 @@ def put(self, key): class MemberStatus(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self, key): """ @@ -303,24 +293,24 @@ def post(self, key): if not isinstance(req_data, dict): return err_response("Valid JSON body object required.") - status = req_data.get("status", "") - if not isinstance(status, str) or not status: + member_status = req_data.get("status", "") + if not isinstance(member_status, str) or not member_status: return err_response("Valid status string required.") - if status != STATUS_ACTIVE and status != STATUS_SUSPENDED: - return err_response("Status must be either " + STATUS_ACTIVE + " or " + STATUS_SUSPENDED) + if member_status != MEMBER_STATUS_ACTIVE and member_status != MEMBER_STATUS_SUSPENDED: + return err_response("Status must be either " + MEMBER_STATUS_ACTIVE + " or " + MEMBER_STATUS_SUSPENDED) member = db.session.query(Member_db).filter_by(username=key).first() if member is None: return err_response(msg="No member found with key " + key, code=404) - old_status = member.status if member.status is not None else STATUS_SUSPENDED - activated = old_status == STATUS_SUSPENDED and status == STATUS_ACTIVE - deactivated = old_status == STATUS_ACTIVE and status == STATUS_SUSPENDED + old_status = member.status if member.status is not None else MEMBER_STATUS_SUSPENDED + activated = old_status == MEMBER_STATUS_SUSPENDED and member_status == MEMBER_STATUS_ACTIVE + deactivated = old_status == MEMBER_STATUS_ACTIVE and member_status == MEMBER_STATUS_SUSPENDED if activated or deactivated: - member.status = status + member.status = member_status db.session.commit() gitlab_account = github_util.sync_gitlab_account( activated, @@ -354,7 +344,7 @@ def post(self, key): class Self(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): authorized_user = get_authorized_user() @@ -388,7 +378,7 @@ def get(self): class PublicSshKeyUpload(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): if 'file' not in request.files: log.error('Upload attempt with no file') @@ -411,7 +401,7 @@ def post(self): return json.loads(member_schema.dumps(member)) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def delete(self): member = get_authorized_user() @@ -430,7 +420,7 @@ def delete(self): class Secrets(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): try: @@ -448,11 +438,11 @@ def get(self): } for s in secrets] return result - except Exception as ex: - return err_response(ex.message, status.HTTP_500_INTERNAL_SERVER_ERROR) + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def post(self): try: @@ -488,15 +478,15 @@ def post(self): 'secret_name': secret_name }, status.HTTP_200_OK - except Exception as ex: - return err_response(ex.message, status.HTTP_500_INTERNAL_SERVER_ERROR) + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) @ns.route('/self/secrets/') class Secrets(Resource): @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, name): try: @@ -518,11 +508,11 @@ def get(self, name): return result, status.HTTP_200_OK - except Exception as ex: - return err_response(ex.message, status.HTTP_500_INTERNAL_SERVER_ERROR) + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def delete(self, name): try: @@ -536,8 +526,8 @@ def delete(self, name): db.session.commit() return custom_response("Successfully deleted secret {}".format(name)) - except Exception as ex: - return err_response(ex.message, status.HTTP_500_INTERNAL_SERVER_ERROR) + except SQLAlchemyError as ex: + return err_response(ex, status.HTTP_500_INTERNAL_SERVER_ERROR) @ns.route('/self/presignedUrlS3//') @@ -547,7 +537,7 @@ class PresignedUrlS3(Resource): expiration_param.add_argument('ws', type=str, required=False, default="") @api.doc(security='ApiKeyAuth') - @login_required + @login_required() @api.expect(expiration_param) def get(self, bucket, key): @@ -590,7 +580,7 @@ class AwsAccessRequesterPaysBucket(Resource): expiration_param.add_argument('exp', type=int, required=False, default=60 * 60 * 12) @api.doc(security='ApiKeyAuth') - @login_required + @login_required() @api.expect(expiration_param) def get(self): member = get_authorized_user() @@ -627,7 +617,7 @@ class AwsAccessEdcCredentials(Resource): """ @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self, endpoint_uri): s = requests.Session() maap_user = get_authorized_user() @@ -669,7 +659,7 @@ class AwsAccessUserBucketCredentials(Resource): """ @api.doc(security='ApiKeyAuth') - @login_required + @login_required() def get(self): maap_user = get_authorized_user() diff --git a/api/utils/http_util.py b/api/utils/http_util.py new file mode 100644 index 0000000..66b1579 --- /dev/null +++ b/api/utils/http_util.py @@ -0,0 +1,10 @@ +from flask_api import status + +def custom_response(msg, code=status.HTTP_200_OK): + return { + 'code': code, + 'message': msg + }, code + +def err_response(msg, code=status.HTTP_400_BAD_REQUEST): + return custom_response(msg, code) \ No newline at end of file