Skip to content

Commit

Permalink
Login/auth system
Browse files Browse the repository at this point in the history
Add user object + validations
Add login/registration/logout code
Add authentication code
Update tests to handle auth
Add tests for auth
Add pdiff tests
  • Loading branch information
ihodes committed Jul 2, 2015
1 parent 272bf79 commit 4b831f1
Show file tree
Hide file tree
Showing 59 changed files with 796 additions and 234 deletions.
2 changes: 2 additions & 0 deletions ENVTEMPLATE.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export WEBHDFS_USER=username
export WEBHDFS_URL=http://example.com:5000
export IGV_HTTPFS_URL=http://example.com:9876
export ALLOW_VCF_OVERWRITES=False
export BCRYPT_LOG_ROUNDS=12
export SECRET_KEY # Set to something real + random for deployment

# True for automatic reloading & debugging JS insertion.
export USE_RELOADER=False
Expand Down
20 changes: 16 additions & 4 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import os
import subprocess


def handle_false(value):
# ensure that false in config isn't interpreted as True
if value and value.lower() == 'false':
value = False
return value
if value and value.lower() == 'false' or not value:
return False
return True


USE_RELOADER = handle_false(os.environ.get('USE_RELOADER', False))
SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URI']
Expand All @@ -18,7 +21,6 @@ def handle_false(value):

TYPEKIT_URL = os.environ.get('TYPEKIT_URL', None)

import subprocess
try:
DEPLOYED_GIT_HASH = subprocess.check_output(['git', 'rev-parse', 'HEAD'])[:-1]
except subprocess.CalledProcessError:
Expand All @@ -29,4 +31,14 @@ def handle_false(value):
TRAVIS = os.environ.get('TRAVIS')
ENSEMBL_RELEASE = os.environ.get('ENSEMBL_RELEASE', 75)

SECRET_KEY = os.environ['SECRET_KEY']
BCRYPT_LOG_ROUNDS = int(os.environ.get('BCRYPT_LOG_ROUNDS', 10))

# Used to disable the @login_required decorator for e.g. seltest
# cf. http://flask-login.readthedocs.org/en/latest/ "Protecting views"
LOGIN_DISABLED = handle_false(os.environ.get('LOGIN_DISABLED', False))


del os
del subprocess
del handle_false
15 changes: 12 additions & 3 deletions cycledash/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy
from flask.ext import restful
from flask.ext import restful, login, bcrypt
import humanize
import logging
import sys
Expand All @@ -12,10 +12,20 @@ def initialize_application():
_configure_application(app)
_configure_logging(app)
_configure_templates(app)
_configure_extensions(app)

return app


def _configure_extensions(app):
global db, api, login_manager, bcrypt
db = SQLAlchemy(app)
api = restful.Api(app, prefix='/api', catch_all_404s=True)
bcrypt = bcrypt.Bcrypt(app)
login_manager = login.LoginManager()
login_manager.init_app(app)


def _configure_logging(app):
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.INFO)
Expand All @@ -39,8 +49,7 @@ def humanize_date(time):


app = initialize_application()
db = SQLAlchemy(app)
api = restful.Api(app, prefix='/api', catch_all_404s=True)


import cycledash.views
import cycledash.auth
36 changes: 36 additions & 0 deletions cycledash/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from flask import request
import flask.ext.restful
from flask.ext.login import current_user

from cycledash import login_manager
from cycledash.auth import check_login


class Resource(flask.ext.restful.Resource, object):
"""Extends Resource by adding an authentication check for basic auth or
valid session cokie.
"""
def __init__(self, *args, **kwargs):
self.auth = getattr(self, 'require_auth', False)
super(Resource, self).__init__(*args, **kwargs)

def dispatch_request(self, *args, **kwargs):
if self.auth:
authorized = False
if login_manager._login_disabled:
# This is used for tests.
authorized = True
elif current_user.is_authenticated():
authorized = True
elif request.authorization:
username = request.authorization.username
password = request.authorization.password
if check_login(username, password):
authorized = True
if not authorized:
auth_msg = 'Correct username/password required.'
return flask.ext.restful.abort(401, message=auth_msg)
return super(Resource, self).dispatch_request(*args, **kwargs)


import projects, bams, runs, genotypes, tasks, comments
10 changes: 7 additions & 3 deletions cycledash/bams.py → cycledash/api/bams.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Defines the API for BAMs."""
from flask import request
from flask.ext.restful import abort, Resource, fields
from flask.ext.restful import abort, fields
from sqlalchemy import select, desc
import voluptuous

from common.helpers import tables, find
from cycledash.validations import CreateBam, UpdateBam, expect_one_of
from cycledash import db
from cycledash.helpers import validate_with, abort_if_none_for, marshal_with
import cycledash.projects
import workers.indexer

import projects
from . import Resource


bam_fields = {
"id": fields.Integer,
Expand All @@ -25,6 +27,7 @@


class BamList(Resource):
require_auth = True
@marshal_with(bam_fields, envelope='bams')
def get(self):
"""Get list of all BAMs."""
Expand All @@ -44,7 +47,7 @@ def post(self):
errors = [str(err) for err in e.errors]
abort(409, message='Validation error', errors=errors)
try:
cycledash.projects.set_and_verify_project_id_on(request.validated_body)
projects.set_and_verify_project_id_on(request.validated_body)
except voluptuous.Invalid as e:
abort(404, message='Project not found.', error=str(e))
with tables(db.engine, 'bams') as (con, bams):
Expand All @@ -56,6 +59,7 @@ def post(self):


class Bam(Resource):
require_auth = True
@marshal_with(bam_fields)
def get(self, bam_id):
"""Get a BAM by its ID."""
Expand Down
7 changes: 6 additions & 1 deletion cycledash/comments.py → cycledash/api/comments.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""API for user comments."""
from collections import defaultdict
from flask import jsonify, request
from flask.ext.restful import abort, Resource, fields
from flask.ext.restful import abort, fields
from sqlalchemy import select, func, desc

from common.helpers import tables, to_epoch
Expand All @@ -11,6 +11,8 @@
marshal_with, camelcase_dict)
from cycledash.validations import CreateComment, DeleteComment, UpdateComment

from . import Resource


comment_fields = {
"id": fields.Integer,
Expand All @@ -28,6 +30,7 @@


class CommentList(Resource):
require_auth = True
@marshal_with(comment_fields, envelope='comments')
def get(self, run_id):
"""Get a list of all comments."""
Expand All @@ -49,6 +52,7 @@ def post(self, run_id):


class Comment(Resource):
require_auth = True
@marshal_with(comment_fields)
def get(self, run_id, comment_id):
"""Get comment with the given ID."""
Expand Down Expand Up @@ -93,6 +97,7 @@ def delete(self, run_id, comment_id):


class CommentsForVcf(Resource):
require_auth = True
def get(self, run_id):
"""Returns comments keys by their record ID."""
# Ideally, this would be done on the client-side with the basic API
Expand Down
5 changes: 3 additions & 2 deletions cycledash/genotypes.py → cycledash/api/genotypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
from collections import OrderedDict
import copy
import json

from flask import request
import flask.ext.restful as restful
from flask.ext.restful import Resource
from sqlalchemy import (select, func, types, cast, join, outerjoin, asc, desc,
and_, Integer, Float, String, distinct)
from sqlalchemy.sql import text
Expand All @@ -17,8 +15,11 @@
from cycledash import db
from common.helpers import tables

from . import Resource


class Genotypes(Resource):
require_auth = True
def get(self, run_id):
return get(run_id, json.loads(request.args.get('q')))

Expand Down
12 changes: 7 additions & 5 deletions cycledash/projects.py → cycledash/api/projects.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
"""Defines the API for Projects."""
from flask import request, redirect, jsonify, url_for, render_template
from flask.ext.restful import Resource, fields, abort
from flask.ext.restful import fields, abort
from sqlalchemy import exc, select, func, desc
import voluptuous

from common.helpers import tables, find
from cycledash import db
import cycledash.tasks
import cycledash.bams
from cycledash.helpers import (get_id_where, abort_if_none_for, validate_with,
marshal_with)
from cycledash.validations import CreateProject, UpdateProject

from . import bams as bamsapi, tasks as tasksapi, Resource


project_fields = {
'id': fields.Integer,
Expand All @@ -21,6 +21,7 @@


class ProjectList(Resource):
require_auth = True
@marshal_with(project_fields, envelope='projects')
def get(self):
"""Get list of all projects."""
Expand All @@ -43,6 +44,7 @@ def post(self):


class Project(Resource):
require_auth = True
@marshal_with(project_fields)
def get(self, project_id):
"""Get a project by its ID."""
Expand Down Expand Up @@ -101,7 +103,7 @@ def get_projects_tree():
q = select(projects.c)
projects = [dict(b) for b in con.execute(q).fetchall()]

cycledash.bams.attach_bams_to_vcfs(vcfs)
bamsapi.attach_bams_to_vcfs(vcfs)

for vcf in vcfs:
project_id = vcf.get('project_id')
Expand All @@ -124,7 +126,7 @@ def get_projects_tree():

def _join_task_states(vcfs):
"""Add a task_states field to each VCF in a list of VCFs."""
ts = cycledash.tasks.all_non_success_tasks()
ts = tasksapi.all_non_success_tasks()

for vcf in vcfs:
vcf['task_states'] = ts.get(vcf['id'], [])
Expand Down
14 changes: 8 additions & 6 deletions cycledash/runs.py → cycledash/api/runs.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from flask import request
from flask.ext.restful import abort, Resource, fields
from flask.ext.restful import abort, fields
from sqlalchemy import select, desc, func
import voluptuous

from cycledash import db, genotypes
from cycledash import db
from cycledash.helpers import (get_id_where, get_where, abort_if_none_for,
validate_with, marshal_with)
import cycledash.bams
import cycledash.projects
from cycledash.validations import CreateRun, UpdateRun, expect_one_of
from common.helpers import tables
import workers.runner

from . import genotypes, bams, projects, Resource


run_fields = {
'id': fields.Integer,
Expand All @@ -35,6 +35,7 @@


class RunList(Resource):
require_auth = True
@marshal_with(run_fields, envelope='runs')
def get(self):
"""Get list of all runs in order of recency."""
Expand All @@ -57,7 +58,7 @@ def post(self):
errors = [str(err) for err in e.errors]
abort(409, message='Validation error', errors=errors)
try:
cycledash.projects.set_and_verify_project_id_on(run)
projects.set_and_verify_project_id_on(run)
except voluptuous.Invalid as e:
abort(404, message='Project not found.', errors=[str(e)])
try:
Expand All @@ -74,13 +75,14 @@ def post(self):


class Run(Resource):
require_auth = True
@marshal_with(thick_run_fields)
def get(self, run_id):
"""Return a vcf with a given ID."""
with tables(db.engine, 'vcfs') as (con, runs):
q = select(runs.c).where(runs.c.id == run_id)
run = dict(_abort_if_none(q.execute().fetchone(), run_id))
cycledash.bams.attach_bams_to_vcfs([run])
bams.attach_bams_to_vcfs([run])
return run

@validate_with(UpdateRun)
Expand Down
6 changes: 5 additions & 1 deletion cycledash/tasks.py → cycledash/api/tasks.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Methods for working with Celery task states."""
from collections import defaultdict
from sqlalchemy import select
from flask.ext.restful import abort, Resource, fields
from flask.ext.restful import abort, fields

from common.helpers import tables
from cycledash.helpers import marshal_with
from cycledash import db
from workers.shared import update_tasks_table, worker
import workers.runner

from . import Resource


task_fields = {
'state': fields.String,
Expand All @@ -18,6 +20,7 @@


class TaskList(Resource):
require_auth = True
@marshal_with(task_fields, envelope='tasks')
def get(self, run_id):
with tables(db.engine, 'task_states') as (con, tasks):
Expand All @@ -37,6 +40,7 @@ def delete(self, run_id):


class TasksRestart(Resource):
require_auth = True
def post(self, run_id):
restart_failed_tasks_for(run_id)
return {'message': 'Restarting failed tasks (run_id={})'.format(run_id)}
Expand Down

0 comments on commit 4b831f1

Please sign in to comment.