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

Implement secret management in MAAP API #134

Closed
wants to merge 5 commits into from
Closed
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
File renamed without changes.
94 changes: 1 addition & 93 deletions api/cas/cas_auth.py → api/auth/cas_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

import flask
import requests
from flask import abort, request, Response, json
from flask import request, json
from flask_api import status
from xmltodict import parse
from flask import current_app
from .cas_urls import create_cas_proxy_url, create_cas_validate_url, create_cas_proxy_validate_url
from api.maap_database import db
from api.models.member import Member
from api.models.member_session import MemberSession
from api.models.member_job import MemberJob
from api import settings
from functools import wraps
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto import Random
Expand All @@ -21,7 +19,6 @@
from api.utils.url_util import proxied_url
import ast


try:
from urllib import urlopen
except ImportError:
Expand All @@ -31,7 +28,6 @@

PROXY_TICKET_PREFIX = "PGT-"


def validate(service, ticket):
"""
Will attempt to validate the ticket. If validation fails False
Expand Down Expand Up @@ -132,15 +128,6 @@ def validate_bearer(token):
return None


def validate_cas_request(token):
"""
Will attempt to validate a CAS machine token. Return True if validation succeeds.
"""

current_app.logger.debug("validating cas request token {0}".format(token))
return token == settings.CAS_SECRET_KEY


def validate_cas_request(cas_url):

xml_from_dict = {}
Expand Down Expand Up @@ -214,82 +201,3 @@ def decrypt_proxy_ticket(ticket):
return ''


def get_authorized_user():
if 'proxy-ticket' in request.headers:
member_session = validate_proxy(request.headers['proxy-ticket'])

if member_session is not None:
return member_session.member

if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
authorized = validate_bearer(token)

if authorized is not None:
return authorized

return None


def login_required(wrapped_function):
@wraps(wrapped_function)
def wrap(*args, **kwargs):

if 'proxy-ticket' in request.headers:
authorized = validate_proxy(request.headers['proxy-ticket'])

if authorized is not None:
return wrapped_function(*args, **kwargs)

if 'cpticket' in request.headers:
authorized = validate_proxy(request.headers['cpticket'])

if authorized is not None:
return wrapped_function(*args, **kwargs)

if 'Authorization' in request.headers:
bearer = request.headers.get('Authorization')
token = bearer.split()[1]
authorized = validate_bearer(token)

if authorized is not None:
return wrapped_function(*args, **kwargs)

if 'cas-authorization' in request.headers:
authorized = validate_cas_request(request.headers['cas-authorization'])

if authorized:
return wrapped_function(*args, **kwargs)

if 'dps-token' in request.headers and valid_dps_request():
return wrapped_function(*args, **kwargs)

abort(status.HTTP_403_FORBIDDEN, description="Not authorized.")

return wrap


def valid_dps_request():
if 'dps-token' in request.headers:
return settings.DPS_MACHINE_TOKEN == request.headers['dps-token']
return False


def edl_federated_request(url, stream_response=False):
s = requests.Session()
response = s.get(url, stream=stream_response)

if response.status_code == status.HTTP_401_UNAUTHORIZED:
maap_user = get_authorized_user()

if maap_user is not None:
urs_token = db.session.query(Member).filter_by(id=maap_user.id).first().urs_token
s.headers.update({'Authorization': f'Bearer {urs_token},Basic {settings.MAAP_EDL_CREDS}',
'Connection': 'close'})

response = s.get(url=response.url, stream=stream_response)

return response


File renamed without changes.
100 changes: 100 additions & 0 deletions api/auth/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from functools import wraps
import requests
from flask import request, abort
from flask_api import status
from api import settings
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

HEADER_PROXY_TICKET = "proxy-ticket"
HEADER_CP_TICKET = "cpticket"
HEADER_AUTHORIZATION = "Authorization"
HEADER_CAS_AUTHORIZATION = "cas-authorization"
HEADER_DPS_TOKEN = "dps-token"


def get_authorized_user():
auth = get_auth_header()

if auth == HEADER_PROXY_TICKET or auth == HEADER_CP_TICKET:
member_session = validate_proxy(request.headers[auth])

if member_session is not None:
return member_session.member

if auth == HEADER_AUTHORIZATION:
bearer = request.headers.get(auth)
token = bearer.split()[1]
authorized = validate_bearer(token)

if authorized is not None:
return authorized

return None


def login_required(wrapped_function):
@wraps(wrapped_function)
def wrap(*args, **kwargs):

auth = get_auth_header()

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 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 auth == HEADER_DPS_TOKEN and valid_dps_request():
return wrapped_function(*args, **kwargs)

abort(status.HTTP_403_FORBIDDEN, description="Not authorized.")

return wrap


def valid_dps_request():
if HEADER_DPS_TOKEN in request.headers:
return settings.DPS_MACHINE_TOKEN == request.headers[HEADER_DPS_TOKEN]
return False


def get_auth_header():
if HEADER_PROXY_TICKET in request.headers:
return HEADER_PROXY_TICKET
if HEADER_CP_TICKET in request.headers:
return HEADER_CP_TICKET
if HEADER_AUTHORIZATION in request.headers:
return HEADER_AUTHORIZATION
if HEADER_CAS_AUTHORIZATION in request.headers:
return HEADER_CAS_AUTHORIZATION
if HEADER_DPS_TOKEN in request.headers:
return HEADER_DPS_TOKEN
return None


def edl_federated_request(url, stream_response=False):
s = requests.Session()
response = s.get(url, stream=stream_response)

if response.status_code == status.HTTP_401_UNAUTHORIZED:
maap_user = get_authorized_user()

if maap_user is not None:
urs_token = db.session.query(Member).filter_by(id=maap_user.id).first().urs_token
s.headers.update({'Authorization': f'Bearer {urs_token},Basic {settings.MAAP_EDL_CREDS}',
'Connection': 'close'})

response = s.get(url=response.url, stream=stream_response)

return response
98 changes: 98 additions & 0 deletions api/endpoints/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import logging
from flask_restx import Resource
from flask import request
from flask_api import status
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

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
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
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
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)}


2 changes: 1 addition & 1 deletion api/endpoints/algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import api.utils.hysds_util as hysds
import api.settings as settings
import api.utils.ogc_translate as ogc
from api.cas.cas_auth import get_authorized_user, login_required
from api.auth.security import get_authorized_user, login_required
from api.maap_database import db
from api.models.member_algorithm import MemberAlgorithm
from sqlalchemy import or_, and_
Expand Down
2 changes: 1 addition & 1 deletion api/endpoints/cmr.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from flask_restx import Resource
from flask_api import status
from api.restplus import api
from api.cas.cas_auth import get_authorized_user, edl_federated_request
from api.auth.security import get_authorized_user, edl_federated_request
from api.maap_database import db
from api.models.member import Member
from urllib import parse
Expand Down
Loading