Skip to content

Commit

Permalink
Use authutils module
Browse files Browse the repository at this point in the history
  • Loading branch information
ZakirG committed Mar 5, 2019
1 parent 6c03a18 commit 2b8cbd1
Show file tree
Hide file tree
Showing 8 changed files with 806 additions and 123 deletions.
4 changes: 3 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pytest="*"
flask = "*"
authutils = "*"
boto3 = "*"
pytest-mock = "*"
pytest-flask = "*"

[requires]
python_version = "3.6"
python_version = "3.7"
596 changes: 596 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

43 changes: 21 additions & 22 deletions manifest_service/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,22 @@
from .admin_endpoints import blueprint as admin_bp
from .manifests import blueprint as manifests_bp

def create_app():
app = flask.Flask(__name__)
app.register_blueprint(admin_bp, url_prefix="/admin")
app.register_blueprint(manifests_bp, url_prefix="")

app = flask.Flask(__name__)
app.register_blueprint(admin_bp, url_prefix="/admin")
app.register_blueprint(manifests_bp, url_prefix="")
# load configuration
app.config.from_object("manifest_service.dev_settings")

# try:
# app_init(app)
# except:
# app.logger.exception("Couldn't initialize application, continuing anyway")

return app

app = create_app()

@app.route("/user_endpoint", methods=["GET"])
def do_something_connected():
Expand Down Expand Up @@ -49,29 +60,17 @@ def health_check():
return "Healthy", 200


def app_init(app):
app.logger.info("Initializing app")
start = time.time()
# def app_init(app):
# app.logger.info("Initializing app")
# start = time.time()

# do the necessary here!
# # do the necessary here!

end = int(round(time.time() - start))
app.logger.info("Initialization complete in {} sec".format(end))
# end = int(round(time.time() - start))
# app.logger.info("Initialization complete in {} sec".format(end))


def run_for_development(**kwargs):
app.logger.setLevel(logging.INFO)

# import os
# for key in ["http_proxy", "https_proxy"]:
# if os.environ.get(key):
# del os.environ[key]

# load configuration
app.config.from_object("manifest_service.dev_settings")

try:
app_init(app)
except:
app.logger.exception("Couldn't initialize application, continuing anyway")

app.run(**kwargs)
1 change: 1 addition & 0 deletions manifest_service/dev_settings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# To run locally, fill these values out
FENCE_USER_INFO_URL = "https://fence-service/user/user"
OIDC_ISSUER = "https://fence-service/user/"
MANIFEST_BUCKET_NAME = "the_name_of_the_s3_bucket_where_manifests_are_stored"
7 changes: 0 additions & 7 deletions manifest_service/errors.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
from cdiserrors import *
from authutils.errors import JWTError

class CustomException(APIError):
def __init__(self, message):
self.message = str(message)
self.code = 500



class UserError(APIError):
'''
User error.
Expand Down
158 changes: 105 additions & 53 deletions manifest_service/manifests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,41 @@
import requests
import ntpath
from datetime import date, datetime
from authutils.token.validate import (
current_token,
require_auth_header,
validate_request,
set_current_token
)

blueprint = Blueprint("manifests", __name__)

import boto3
session = boto3.Session(region_name="us-east-1")
s3 = session.resource("s3")

def get_folder_name_from_user_info(user_info):
return "user-" + str(user_info["user_id"])
def get_folder_name_from_token(user_info):
"""
Returns the name of the user's manifest folder (their "prefix").
It takes a "user_info" dict, which is the response that Fence returns at /user/user
The convention we'll use here is that a user's folder name will be "user-x" where x is
their ID (integer).
According to the revproxy's helpers.js, it looks like the user_id is stored in a variable called "sub". Hm.
"""
return "user-" + str(user_info["sub"])

def does_the_user_have_read_access_on_at_least_one_project(project_access_dict):
privileges = list(project_access_dict.values())
def does_the_user_have_read_access_on_at_least_one_project(current_token):
"""
Returns True if the user has both read and read-storage access on at least one project,
False otherwise.
"""
privileges = []
try:
project_access_dict = current_token.get("context").get("user").get("projects")
privileges = list(project_access_dict.values())
except Exception:
return False

if len(privileges) == 0:
return False
Expand All @@ -27,32 +50,12 @@ def does_the_user_have_read_access_on_at_least_one_project(project_access_dict):

return False

def is_the_user_permitted_to_use_this_service(access_token):
def is_valid_manifest(manifest_json):
"""
This function returns (access_token, full_user_info_response_from_fence)
Returns (True, "") if the manifest.json is a list of the form [{'k' : v}, ...],
where valid keys are object_id and subject_id.
Otherwise, returns (False, error_msg)
"""
headers = {
"Accept" : "application/json",
"Content-Type": "application/json",
}

cookies = {
"access_token" : access_token
}

FENCE_USER_INFO_URL = flask.current_app.config.get("FENCE_USER_INFO_URL")
r = requests.get(FENCE_USER_INFO_URL, headers=headers, cookies=cookies)

try:
json = r.json()
except:
return False, None

project_access_dict = json["project_access"]

return does_the_user_have_read_access_on_at_least_one_project(project_access_dict), json

def is_valid_manifest(manifest_json):
valid_keys = set(["object_id" , "subject_id"])
error_msg = "Manifest format is invalid. Please POST a list of key-value pairs, like [{'k' : v}, ...] Valid keys are: " + " ".join(valid_keys)
if type(manifest_json) != list:
Expand All @@ -69,12 +72,21 @@ def is_valid_manifest(manifest_json):
return True, ""

def generate_unique_manifest_filename(folder_name, manifest_bucket_name):
"""
Returns a filename of the form manifest-<timestamp>-<optional-increment>.json that is
unique among the files in the user's manifest folder.
"""
timestamp = datetime.now().isoformat()
users_existing_manifest_files = list_files_in_bucket(manifest_bucket_name, folder_name)
filename = generate_unique_filename_with_timestamp_and_increment(timestamp, users_existing_manifest_files)
return filename

def generate_unique_filename_with_timestamp_and_increment(timestamp, users_existing_manifest_files):
"""
A helper function for generate_unique_manifest_filename(), which facilitates unit testing.
Adds an increment to the filename if there happens to be another timestamped file with the same name
(unlikely, but good to check).
"""
filename_without_extension = "manifest-" + timestamp.replace(":", "-")
extension = ".json"

Expand All @@ -89,6 +101,9 @@ def generate_unique_filename_with_timestamp_and_increment(timestamp, users_exist
return filename

def list_files_in_bucket(bucket_name, folder):
"""
Lists the files in an s3 bucket. Returns a list of filenames.
"""
rv = []
bucket = s3.Bucket(bucket_name)

Expand All @@ -98,6 +113,9 @@ def list_files_in_bucket(bucket_name, folder):
return rv

def get_file_contents(bucket_name, folder, filename):
"""
Returns the body of a requested file as a string.
"""
client = boto3.client("s3")
obj = client.get_object(Bucket=bucket_name, Key=folder + "/" + filename)
as_bytes = obj["Body"].read()
Expand All @@ -107,19 +125,30 @@ def get_file_contents(bucket_name, folder, filename):
@blueprint.route("/", methods=["GET"])
def get_manifests():
"""
Returns a list of filenames corresponding to the user's manifests
Returns a list of filenames corresponding to the user's manifests.
We find the appropriate folder ("prefix") in the bucket by asking Fence for
info about the user's access token.
---
responses:
200:
description: Success
403:
description: Unauthorized
"""
access_token = request.cookies.get("access_token")
if access_token is None:

try:
set_current_token(validate_request(aud={"user"}))
except Exception:
json_to_return = { "error" : "Please log in." }
return flask.jsonify(json_to_return), 403
return flask.jsonify(json_to_return), 401

auth_successful = does_the_user_have_read_access_on_at_least_one_project(current_token)

auth_successful, user_info = is_the_user_permitted_to_use_this_service(access_token)
if not auth_successful:
json_to_return = { "error" : "You must have read access on at least one project in order to use this feature." }
return flask.jsonify(json_to_return), 403
folder_name = get_folder_name_from_user_info(user_info)

folder_name = get_folder_name_from_token(current_token)

MANIFEST_BUCKET_NAME = flask.current_app.config.get("MANIFEST_BUCKET_NAME")
json_to_return = {
Expand All @@ -132,34 +161,47 @@ def get_manifests():
def get_manifest_file(file_name):
"""
Returns the requested manifest file from the user's folder.
---
responses:
200:
description: Success
403:
description: Unauthorized
400:
description: Bad request format
"""
if not file_name.endswith("json"):
json_to_return = { "error" : "Incorrect usage. You can only use this pathway to request files of type JSON." }
return flask.jsonify(json_to_return), 403

access_token = request.cookies.get("access_token")
if access_token is None:
try:
set_current_token(validate_request(aud={"user"}))
except Exception:
json_to_return = { "error" : "Please log in." }
return flask.jsonify(json_to_return), 403
return flask.jsonify(json_to_return), 401

auth_successful = does_the_user_have_read_access_on_at_least_one_project(current_token)

auth_successful, user_info = is_the_user_permitted_to_use_this_service(access_token)
if not auth_successful:
json_to_return = { "error" : "You must have read access on at least one project in order to use this feature." }
return flask.jsonify(json_to_return), 403

folder_name = get_folder_name_from_user_info(user_info)
if not file_name.endswith("json"):
json_to_return = { "error" : "Incorrect usage. You can only use this pathway to request files of type JSON." }
return flask.jsonify(json_to_return), 400

folder_name = get_folder_name_from_token(current_token)

MANIFEST_BUCKET_NAME = flask.current_app.config.get("MANIFEST_BUCKET_NAME")
json_to_return = {
"body" : get_file_contents(MANIFEST_BUCKET_NAME, folder_name, file_name)
}

print(json_to_return)

return flask.jsonify(json_to_return), 200

def add_manifest_to_bucket(user_info, manifest_json):
folder_name = get_folder_name_from_user_info(user_info)
def add_manifest_to_bucket(current_token, manifest_json):
"""
Puts the manifest_json string into a file and uploads it to s3.
Generates and returns the name of the new file.
"""
folder_name = get_folder_name_from_token(current_token)

MANIFEST_BUCKET_NAME = flask.current_app.config.get("MANIFEST_BUCKET_NAME")
filename = generate_unique_manifest_filename(folder_name, MANIFEST_BUCKET_NAME)
Expand All @@ -174,14 +216,24 @@ def add_manifest_to_bucket(user_info, manifest_json):
def put_manifest():
"""
Add manifest to s3 bucket
---
responses:
200:
description: Success
403:
description: Unauthorized
400:
description: Bad manifest format
"""

access_token = request.cookies.get("access_token")
if access_token is None:

try:
set_current_token(validate_request(aud={"user"}))
except Exception:
json_to_return = { "error" : "Please log in." }
return flask.jsonify(json_to_return), 403
return flask.jsonify(json_to_return), 401

auth_successful = does_the_user_have_read_access_on_at_least_one_project(current_token)

auth_successful, user_info = is_the_user_permitted_to_use_this_service(access_token)
if not auth_successful:
json_to_return = { "error" : "You must have read access on at least one project in order to use this feature." }
return flask.jsonify(json_to_return), 403
Expand All @@ -194,7 +246,7 @@ def put_manifest():
if not is_valid:
return flask.jsonify({"error" : err}), 400

filename = add_manifest_to_bucket(user_info, manifest_json)
filename = add_manifest_to_bucket(current_token, manifest_json)

ret = {
"filename": filename,
Expand Down
Loading

0 comments on commit 2b8cbd1

Please sign in to comment.