Skip to content

Commit d12f41c

Browse files
authored
Merge pull request #8 from MatchmakerExchange/exchange_server
Authentication support and refactoring
2 parents 1e888e4 + d5b44c4 commit d12f41c

File tree

22 files changed

+849
-337
lines changed

22 files changed

+849
-337
lines changed

.travis.yml

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
# After changing this file, check it on:
22
# http://lint.travis-ci.org/
33
language: python
4+
45
python:
56
- "2.7"
67
- "3.3"
78
- "3.4"
89
- "3.5"
10+
11+
sudo: false
12+
13+
cache:
14+
apt: true
15+
16+
addons:
17+
apt:
18+
sources:
19+
- elasticsearch-2.x
20+
packages:
21+
- elasticsearch
22+
23+
services:
24+
- elasticsearch
25+
926
install:
1027
- pip install -e .
1128
- mme-server quickstart
29+
1230
script:
1331
# Test via module runner
1432
- python -m mme_server test
1533
# Test via setup.py (with environment variable to also test quickstart)
1634
- MME_TEST_QUICKSTART=1 coverage run --source=mme_server setup.py test
17-
services:
18-
- elasticsearch
35+
1936
before_script:
2037
# Delay for elasticsearch to start
2138
- sleep 10
39+
2240
before_install:
2341
- pip install coveralls
42+
2443
after_success:
25-
- coveralls
44+
- coveralls

MANIFEST.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
include LICENSE.txt
33
include README.md
44
include MANIFEST.in
5-
recursive-include mme_server/schemas *.json
5+
recursive-include mme_server/schemas *.json
6+
recursive-include mme_server/templates *.html

README.md

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ This code is intended to be illustrative and is **not** guaranteed to perform we
1111

1212

1313
## Dependencies
14+
1415
- Python 2.7 or 3.3+
15-
- ElasticSearch
16+
- ElasticSearch 2.x
1617

1718

1819
## Quickstart
@@ -42,12 +43,20 @@ This code is intended to be illustrative and is **not** guaranteed to perform we
4243
mme-server quickstart
4344
```
4445
45-
1. Run tests:
46+
1. Run tests (must run quickstart first):
4647
4748
```sh
4849
mme-server test
4950
```
5051
52+
1. Authorize an incoming server:
53+
54+
```sh
55+
mme-server clients add myclient --label "My Client" --key "<CLIENT_AUTH_TOKEN>"
56+
```
57+
58+
Leave off the `--key` option to have a secure key randomly generated for you.
59+
5160
1. Start up MME reference server:
5261
5362
```sh
@@ -59,14 +68,17 @@ This code is intended to be illustrative and is **not** guaranteed to perform we
5968
1. Try it out:
6069
6170
```sh
62-
curl -XPOST -H 'Content-Type: application/vnd.ga4gh.matchmaker.v1.0+json' \
63-
-H 'Accept: application/vnd.ga4gh.matchmaker.v1.0+json' \
64-
-d '{"patient":{
71+
curl -XPOST \
72+
-H 'X-Auth-Token: <CLIENT_AUTH_TOKEN>' \
73+
-H 'Content-Type: application/vnd.ga4gh.matchmaker.v1.0+json' \
74+
-H 'Accept: application/vnd.ga4gh.matchmaker.v1.0+json' \
75+
-d '{"patient":{
6576
"id":"1",
6677
"contact": {"name":"Jane Doe", "href":"mailto:[email protected]"},
6778
"features":[{"id":"HP:0000522"}],
68-
"genomicFeatures":[{"gene":{"id":"NGLY1"}}]
69-
}}' localhost:8000/match
79+
"genomicFeatures":[{"gene":{"id":"NGLY1"}}],
80+
"test": true
81+
}}' localhost:8000/v1/match
7082
```
7183
7284
## Installation
@@ -93,14 +105,14 @@ source .virtualenv/bin/activate
93105
First, download elasticsearch:
94106

95107
```sh
96-
wget https://download.elasticsearch.org/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.1.1/elasticsearch-2.1.1.tar.gz
97-
tar -xzf elasticsearch-2.1.1.tar.gz
108+
wget https://download.elastic.co/elasticsearch/elasticsearch/elasticsearch-1.7.6.zip
109+
unzip elasticsearch-1.7.6.zip
98110
```
99111

100112
Then, start up a local elasticsearch cluster to serve as our database (`-Des.path.data=data` puts the elasticsearch indices in a subdirectory called `data`):
101113

102114
```sh
103-
./elasticsearch-2.1.1/bin/elasticsearch -Des.path.data=data
115+
./elasticsearch-1.7.6/bin/elasticsearch -Des.path.data=data
104116
```
105117

106118

@@ -117,18 +129,21 @@ Custom patient data can be indexed by the server in two ways (if a patient 'id'
117129
1. Batch index from the Python interface:
118130

119131
```py
120-
>>> from mme_server.models import DatastoreConnection
121-
>>> db = DatastoreConnection()
122-
>>> db.patients.index('/path/to/patients.json')
132+
>>> from mme_server.backend import get_backend
133+
>>> db = get_backend()
134+
>>> patients = db.get_manager('patients')
135+
>>> patients.index('/path/to/patients.json')
123136
```
124137

125138
1. Single patient index the Python interface:
126139

127140
```py
128-
>>> from mme_server.models import Patient, DatastoreConnection
129-
>>> db = DatastoreConnection()
141+
>>> from mme_server.backend import get_backend
142+
>>> db = get_backend()
143+
>>> patients = db.get_manager('patients')
144+
>>> from mme_server.models import Patient
130145
>>> patient = Patient.from_api({...})
131-
>>> db.patients.index_patient(patient)
146+
>>> patients.index_patient(patient)
132147
```
133148

134149

@@ -142,8 +157,3 @@ If you have any questions, feel free to post an issue on GitHub.
142157
This repository is managed by the Matchmaker Exchange technical team. You can reach us via GitHub or by [email](mailto:[email protected]).
143158

144159
Contributions are most welcome! Post an issue, submit a bugfix, or just try it out. We hope you find it useful.
145-
146-
147-
## Implementations
148-
149-
We don't know of any organizations using this code in a production setting just yet. If you are, please let us know! We'd love to list you here.

mme_server/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .cli import main
2+
from .server import app

mme_server/auth.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Module hanlding the authentication of incoming and outgoing server requests.
3+
4+
Stores:
5+
* Authenticated servers (`servers` index)
6+
"""
7+
from __future__ import with_statement, division, unicode_literals
8+
9+
import logging
10+
import flask
11+
12+
from functools import wraps
13+
14+
from flask import request, jsonify
15+
16+
from .backend import get_backend
17+
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def auth_token_required():
23+
def decorator(f):
24+
@wraps(f)
25+
def decorated_function(*args, **kwargs):
26+
logger.info("Authenticating request")
27+
token = request.headers.get('X-Auth-Token')
28+
backend = get_backend()
29+
servers = backend.get_manager('servers')
30+
server = servers.verify(token)
31+
if not server:
32+
error = jsonify(message='X-Auth-Token not authorized')
33+
error.status_code = 401
34+
return error
35+
36+
# Set authenticated server as flask global for request
37+
flask.g.server = server
38+
return f(*args, **kwargs)
39+
return decorated_function
40+
return decorator

mme_server/backend.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Module for accessing the backend connection
3+
"""
4+
5+
from __future__ import with_statement, division, unicode_literals
6+
7+
import logging
8+
import flask
9+
10+
from elasticsearch import Elasticsearch
11+
12+
from .managers import Managers
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
18+
def get_backend():
19+
backend = getattr(flask.g, '_mme_backend', None)
20+
if backend is None:
21+
backend = flask.g._mme_backend = Managers(Elasticsearch())
22+
23+
return backend

mme_server/cli.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import logging
99
import unittest
1010

11+
from binascii import hexlify
12+
13+
from .backend import get_backend
1114
from .compat import urlretrieve
12-
from .models import get_backend
1315
from .server import app
1416

1517

@@ -56,10 +58,12 @@ def index_file(index, filename, url):
5658

5759
with app.app_context():
5860
backend = get_backend()
61+
patients = backend.get_manager('patients')
62+
vocabularies = backend.get_manager('vocabularies')
5963
index_funcs = {
60-
'hpo': backend.vocabularies.index_hpo,
61-
'genes': backend.vocabularies.index_genes,
62-
'patients': backend.patients.index,
64+
'hpo': vocabularies.index_hpo,
65+
'genes': vocabularies.index_genes,
66+
'patients': patients.index_file,
6367
}
6468
index_funcs[index](filename=filename)
6569

@@ -73,11 +77,87 @@ def fetch_resource(filename, url):
7377
logger.info('Saved file to: {}'.format(filename))
7478

7579

80+
def list_servers(direction='out'):
81+
with app.app_context():
82+
backend = get_backend()
83+
servers = backend.get_manager('servers')
84+
response = servers.list(direction=direction)
85+
# print header
86+
fields = response['fields']
87+
print('\t'.join(fields))
88+
89+
for server in response.get('rows', []):
90+
print('\t'.join([repr(server[field]) for field in fields]))
91+
92+
def list_clients():
93+
return list_servers(direction='in')
94+
95+
def add_server(id, direction='out', key=None, label=None, base_url=None):
96+
if not label:
97+
label = id
98+
99+
if direction == 'out' and not base_url:
100+
raise Exception('base-url must be specified for outgoing servers')
101+
102+
with app.app_context():
103+
backend = get_backend()
104+
servers = backend.get_manager('servers')
105+
# Generate a random key if one was not provided
106+
if key is None:
107+
key = hexlify(os.urandom(30)).decode()
108+
servers.add(server_id=id, server_key=key, direction=direction, server_label=label, base_url=base_url)
109+
110+
def add_client(id, key=None, label=None):
111+
add_server(id, 'in', key=key, label=label)
112+
113+
def remove_server(id, direction='out'):
114+
with app.app_context():
115+
backend = get_backend()
116+
servers = backend.get_manager('servers')
117+
servers.remove(server_id=id, direction=direction)
118+
119+
def remove_client(id):
120+
remove_server(id, direction='in')
121+
122+
76123
def run_tests():
77124
suite = unittest.TestLoader().discover('.'.join([__package__, 'tests']))
78125
unittest.TextTestRunner().run(suite)
79126

80127

128+
def add_server_subcommands(parser, direction):
129+
"""Add subparser for incoming or outgoing servers
130+
131+
direction - 'in': incoming servers, 'out': outgoing servers
132+
"""
133+
server_type = 'client' if direction == 'in' else 'server'
134+
subparsers = parser.add_subparsers(title='subcommands')
135+
subparser = subparsers.add_parser('add', description="Add {} authorization".format(server_type))
136+
subparser.add_argument("id", help="A unique {} identifier".format(server_type))
137+
if server_type == 'server':
138+
subparser.add_argument("base_url", help="The base HTTPS URL for sending API requests to the server (e.g., <base-url>/match should be a valid endpoint).")
139+
140+
subparser.add_argument("--key", help="The secret key used to authenticate requests to/from the {} (default: randomly generate a secure key)".format(server_type))
141+
subparser.add_argument("--label", help="The display name for the {}".format(server_type))
142+
if server_type == 'server':
143+
subparser.set_defaults(function=add_server)
144+
else:
145+
subparser.set_defaults(function=add_client)
146+
147+
subparser = subparsers.add_parser('rm', description="Remove {} authorization".format(server_type))
148+
subparser.add_argument("id", help="The {} identifier".format(server_type))
149+
if server_type == 'server':
150+
subparser.set_defaults(function=remove_server)
151+
else:
152+
subparser.set_defaults(function=remove_client)
153+
154+
subparser = subparsers.add_parser('list', description="List {} authorizations".format(server_type))
155+
if server_type == 'server':
156+
subparser.set_defaults(function=list_servers)
157+
else:
158+
subparser.set_defaults(function=list_clients)
159+
160+
81161
def parse_args(args):
82162
from argparse import ArgumentParser
83163

@@ -116,6 +196,12 @@ def parse_args(args):
116196
help="The host the server will listen to (0.0.0.0 to listen globally; 127.0.0.1 to listen locally; default: %(default)s)")
117197
subparser.set_defaults(function=app.run)
118198

199+
subparser = subparsers.add_parser('servers', description="Server authorization sub-commands")
200+
add_server_subcommands(subparser, direction='out')
201+
202+
subparser = subparsers.add_parser('clients', description="Client authorization sub-commands")
203+
add_server_subcommands(subparser, direction='in')
204+
119205
subparser = subparsers.add_parser('test', description="Run tests")
120206
subparser.set_defaults(function=run_tests)
121207

mme_server/compat.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,13 @@
66
from urllib import urlretrieve
77
except ImportError:
88
from urllib.request import urlretrieve
9+
10+
try:
11+
from urllib2 import urlopen, Request
12+
except ImportError:
13+
from urllib.request import urlopen, Request
14+
15+
try:
16+
from urlparse import urlsplit
17+
except ImportError:
18+
from urllib.parse import urlsplit

0 commit comments

Comments
 (0)