Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User secret management #135

Merged
merged 3 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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