Skip to content

Commit

Permalink
User secret management (#135)
Browse files Browse the repository at this point in the history
* Add user secrets endpoints

* Edit secret deletion error message

Co-authored-by: Sujen Shah <[email protected]>

* Restrict login_required decorator to specific roles; Improved permissions error handling; minor refactoring

---------

Co-authored-by: bsatoriu <[email protected]>
Co-authored-by: Sujen Shah <[email protected]>
  • Loading branch information
3 people authored Aug 19, 2024
1 parent ce6a216 commit 1d0e59c
Show file tree
Hide file tree
Showing 13 changed files with 972 additions and 646 deletions.
45 changes: 25 additions & 20 deletions api/auth/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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:
Expand Down
97 changes: 97 additions & 0 deletions api/endpoints/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import logging
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
from api.models.pre_approved import PreApproved
from api.schemas.pre_approved_schema import PreApprovedSchema
from datetime import datetime
import json

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(role=Role.ROLE_ADMIN)
def get(self):
pre_approved = db.session.query(
PreApproved.email,
PreApproved.creation_date
).order_by(PreApproved.email).all()

pre_approved_schema = PreApprovedSchema()
result = [json.loads(pre_approved_schema.dumps(p)) for p in pre_approved]
return result

@api.doc(security='ApiKeyAuth')
@login_required(role=Role.ROLE_ADMIN)
def post(self):

"""
Create new pre-approved email. Wildcards are supported for starting email characters.
Format of JSON to post:
{
"email": ""
}
Sample 1. Any email ending in "@maap-project.org" is pre-approved
{
"email": "*@maap-project.org"
}
Sample 2. Any email matching "[email protected]" is pre-approved
{
"email": "[email protected]"
}
"""

req_data = request.get_json()
if not isinstance(req_data, dict):
return err_response("Valid JSON body object required.")

email = req_data.get("email", "")
if not isinstance(email, str) or not email:
return err_response("Valid email is required.")

pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first()

if pre_approved_email is not None:
return err_response(msg="Email already exists")

new_email = PreApproved(email=email, creation_date=datetime.utcnow())

db.session.add(new_email)
db.session.commit()

pre_approved_schema = PreApprovedSchema()
return json.loads(pre_approved_schema.dumps(new_email))


@ns.route('/pre-approved/<string:email>')
class PreApprovedEmails(Resource):

@api.doc(security='ApiKeyAuth')
@login_required(role=Role.ROLE_ADMIN)
def delete(self, email):
"""
Delete pre-approved email
"""

pre_approved_email = db.session.query(PreApproved).filter_by(email=email).first()

if pre_approved_email is None:
return err_response(msg="Email does not exist")

db.session.query(PreApproved).filter_by(email=email).delete()
db.session.commit()

return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(email)}
9 changes: 5 additions & 4 deletions api/endpoints/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions api/endpoints/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 1d0e59c

Please sign in to comment.