Skip to content

Commit 1b8b0c7

Browse files
authored
Separate plugin file system from BASA_DATA_DIR (#3480)
* extended configuration manager with optional OIDC sections * flake8 * also provide a label for a speaking name of the identity provider * start implementing the OIDC dance * modal not necessary, if only one provider was defined * error handling of provider not in config file * adding pycurl package to enable tornado curl_httpclients * a new method to create a user, if information do not need to be entered manually, but are obtained from an external identity provider * full OIDC dance implemented * add an admin page to activate users which requested authorization through OIDC * flake8 * adding menu entry for user authorization * do not expose traditional qiita internal user authentication, if OIDC is configured * use Qiita typical modal for OIDC login * wrong menu entrie affected * always allow logout * improved error handling * revert: let user change their profile, but not password - if provided through OIDC * speaking button names + move into correct div to always get displayed * use email from config + loop user_info from OIDC to fill DB * use OIDC info to prefil user information * drop admin user authorization * using the well-known json dict instead of manually providing multiple API endpoints through the config file * flake8 * flake8 * add ability to display OIDC logos * add OIDC logo * revert to dev branch * fixing config manager tests * add missing template * extended configuration manager with optional OIDC sections * flake8 * also provide a label for a speaking name of the identity provider * start implementing the OIDC dance * modal not necessary, if only one provider was defined * error handling of provider not in config file * adding pycurl package to enable tornado curl_httpclients * a new method to create a user, if information do not need to be entered manually, but are obtained from an external identity provider * full OIDC dance implemented * add an admin page to activate users which requested authorization through OIDC * flake8 * adding menu entry for user authorization * do not expose traditional qiita internal user authentication, if OIDC is configured * use Qiita typical modal for OIDC login * always allow logout * improved error handling * revert: let user change their profile, but not password - if provided through OIDC * speaking button names + move into correct div to always get displayed * use email from config + loop user_info from OIDC to fill DB * use OIDC info to prefil user information * drop admin user authorization * using the well-known json dict instead of manually providing multiple API endpoints through the config file * flake8 * flake8 * add ability to display OIDC logos * add OIDC logo * fixing config manager tests * multiple validation jobs should be submitted as lead and dependent jobs, but the later must also made known by the DB * expose additional HTTP endpoints to send and retrieve data over http from/to plugin * add another configuration variable to decide if HTTPS filetransfer endpoint will be exposed * aim to subtract OIDC changes * remove image * restore blank line * revert changes * revert * revert * return info as "reason" * initial set of tests * codestyle * more codestyle * enable endpoints * tests pass locally * adding in more functionallity for testing, i.e. operate on token based authorization * avoid name collisions * debug * more debug * change debug * continue debug * adapt filepaths to github runner * remove debug and fix codestyle * remove debug step * after talking with Antonio, we decided that no additional config parameter for ENABLE_HTTPS_PLUGIN_FILETRANSFER is necessary, we simply expose these new endpoints, whatever * Update config_test.cfg
1 parent a34dceb commit 1b8b0c7

File tree

6 files changed

+253
-1
lines changed

6 files changed

+253
-1
lines changed

.github/workflows/qiita-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ jobs:
148148
149149
echo "4. Setting up nginx"
150150
mkdir -p /usr/share/miniconda/envs/qiita/var/run/nginx/
151+
sed -i "s|alias /Users/username|alias /home/runner/work/qiita|" ${PWD}/qiita_pet/nginx_example.conf
151152
nginx -c ${PWD}/qiita_pet/nginx_example.conf
152153
153154
echo "5. Setting up qiita"

qiita_db/handlers/tests/oauthbase.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# -----------------------------------------------------------------------------
88

9-
from qiita_core.qiita_settings import r_client
9+
import requests
10+
import os
11+
import sys
12+
from qiita_core.qiita_settings import r_client, qiita_config
1013

1114
from qiita_pet.test.tornado_test_base import TestHandlerBase
1215

@@ -19,3 +22,49 @@ def setUp(self):
1922
r_client.hset(self.token, 'grant_type', 'client')
2023
r_client.expire(self.token, 20)
2124
super(OauthTestingBase, self).setUp()
25+
self._session = requests.Session()
26+
# should point to client certificat file:
27+
# /qiita/qiita_core/support_files/ci_rootca.crt
28+
self._verify = os.environ['QIITA_ROOTCA_CERT']
29+
self._fetch_token()
30+
31+
self._files_to_remove = []
32+
33+
def tearDown(self):
34+
for fp in self._files_to_remove:
35+
if os.path.exists(fp):
36+
os.remove(fp)
37+
38+
def _fetch_token(self):
39+
data = {
40+
'client_id': '4MOBzUBHBtUmwhaC258H7PS0rBBLyGQrVxGPgc9g305bvVhf6h',
41+
'client_secret':
42+
('rFb7jwAb3UmSUN57Bjlsi4DTl2owLwRpwCc0SggRN'
43+
'EVb2Ebae2p5Umnq20rNMhmqN'),
44+
'grant_type': 'client'}
45+
resp = self._session.post(
46+
"%s/qiita_db/authenticate/" % qiita_config.base_url,
47+
verify=self._verify, data=data, timeout=80)
48+
if resp.status_code != 200:
49+
raise ValueError("_fetchToken() POST request failed")
50+
self._token = resp.json()['access_token']
51+
print('obtained access_token = %s' % self._token, file=sys.stderr)
52+
53+
def post_authed(self, url, **kwargs):
54+
if 'headers' not in kwargs:
55+
kwargs['headers'] = {}
56+
if 'Authorization' not in kwargs['headers']:
57+
kwargs['headers']['Authorization'] = 'Bearer %s' % self._token
58+
59+
r = self._session.post(
60+
qiita_config.base_url + url, verify=self._verify, **kwargs)
61+
r.close()
62+
63+
return r
64+
65+
def get_authed(self, url):
66+
r = self._session.get(qiita_config.base_url + url, verify=self._verify,
67+
headers={'Authorization': 'Bearer %s' %
68+
self._token})
69+
r.close()
70+
return r
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from .file_transfer_handlers import (FetchFileFromCentralHandler,
2+
PushFileToCentralHandler)
3+
4+
__all__ = ['FetchFileFromCentralHandler']
5+
6+
ENDPOINTS = [
7+
(r"/cloud/fetch_file_from_central/(.*)", FetchFileFromCentralHandler),
8+
(r"/cloud/push_file_to_central/", PushFileToCentralHandler)
9+
]
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import os
2+
3+
from tornado.web import HTTPError, RequestHandler
4+
from tornado.gen import coroutine
5+
6+
from qiita_core.util import execute_as_transaction
7+
from qiita_db.handlers.oauth2 import authenticate_oauth
8+
from qiita_core.qiita_settings import qiita_config
9+
10+
11+
class FetchFileFromCentralHandler(RequestHandler):
12+
@authenticate_oauth
13+
@coroutine
14+
@execute_as_transaction
15+
def get(self, requested_filepath):
16+
# ensure we have an absolute path, i.e. starting at /
17+
filepath = os.path.join(os.path.sep, requested_filepath)
18+
# use a canonic version of the filepath
19+
filepath = os.path.abspath(filepath)
20+
21+
# canonic version of base_data_dir
22+
basedatadir = os.path.abspath(qiita_config.base_data_dir)
23+
24+
# TODO: can we somehow check, if the requesting client (which should be
25+
# one of the plugins) was started from a user that actually has
26+
# access to the requested file?
27+
28+
if not filepath.startswith(basedatadir):
29+
# attempt to access files outside of the BASE_DATA_DIR
30+
# intentionally NOT reporting the actual location to avoid exposing
31+
# instance internal information
32+
raise HTTPError(403, reason=(
33+
"You cannot access files outside of "
34+
"the BASE_DATA_DIR of Qiita!"))
35+
36+
if not os.path.exists(filepath):
37+
raise HTTPError(403, reason=(
38+
"The requested file is not present in Qiita's BASE_DATA_DIR!"))
39+
40+
# delivery of the file via nginx requires replacing the basedatadir
41+
# with the prefix defined in the nginx configuration for the
42+
# base_data_dir, '/protected/' by default
43+
protected_filepath = filepath.replace(basedatadir, '/protected')
44+
45+
self.set_header('Content-Type', 'application/octet-stream')
46+
self.set_header('Content-Transfer-Encoding', 'binary')
47+
self.set_header('X-Accel-Redirect', protected_filepath)
48+
self.set_header('Content-Description', 'File Transfer')
49+
self.set_header('Expires', '0')
50+
self.set_header('Cache-Control', 'no-cache')
51+
self.set_header('Content-Disposition',
52+
'attachment; filename=%s' % os.path.basename(
53+
protected_filepath))
54+
self.finish()
55+
56+
57+
class PushFileToCentralHandler(RequestHandler):
58+
@authenticate_oauth
59+
@coroutine
60+
@execute_as_transaction
61+
def post(self):
62+
if not self.request.files:
63+
raise HTTPError(400, reason='No files to upload defined!')
64+
65+
# canonic version of base_data_dir
66+
basedatadir = os.path.abspath(qiita_config.base_data_dir)
67+
stored_files = []
68+
69+
for filespath, filelist in self.request.files.items():
70+
if filespath.startswith(basedatadir):
71+
filespath = filespath[len(basedatadir):]
72+
73+
for file in filelist:
74+
filepath = os.path.join(filespath, file['filename'])
75+
# remove leading /
76+
if filepath.startswith(os.sep):
77+
filepath = filepath[len(os.sep):]
78+
filepath = os.path.abspath(os.path.join(basedatadir, filepath))
79+
80+
if os.path.exists(filepath):
81+
raise HTTPError(403, reason=(
82+
"The requested file is already "
83+
"present in Qiita's BASE_DATA_DIR!"))
84+
85+
os.makedirs(os.path.dirname(filepath), exist_ok=True)
86+
with open(filepath, "wb") as f:
87+
f.write(file['body'])
88+
stored_files.append(filepath)
89+
90+
self.write("Stored %i files into BASE_DATA_DIR of Qiita:\n%s\n" % (
91+
len(stored_files),
92+
'\n'.join(map(lambda x: ' - %s' % x, stored_files))))
93+
94+
self.finish()
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from unittest import main
2+
from os.path import exists, basename
3+
from os import remove
4+
import filecmp
5+
6+
from qiita_db.handlers.tests.oauthbase import OauthTestingBase
7+
import qiita_db as qdb
8+
9+
10+
class FetchFileFromCentralHandlerTests(OauthTestingBase):
11+
def setUp(self):
12+
super(FetchFileFromCentralHandlerTests, self).setUp()
13+
14+
def test_get(self):
15+
endpoint = '/cloud/fetch_file_from_central/'
16+
base_data_dir = qdb.util.get_db_files_base_dir()
17+
18+
obs = self.get_authed(endpoint + 'nonexistingfile')
19+
self.assertEqual(obs.status_code, 403)
20+
self.assertIn('outside of the BASE_DATA_DIR', obs.reason)
21+
22+
obs = self.get_authed(
23+
endpoint + base_data_dir[1:] + '/nonexistingfile')
24+
self.assertEqual(obs.status_code, 403)
25+
self.assertIn('The requested file is not present', obs.reason)
26+
27+
obs = self.get_authed(
28+
endpoint + base_data_dir[1:] +
29+
'/raw_data/FASTA_QUAL_preprocessing.fna')
30+
self.assertEqual(obs.status_code, 200)
31+
self.assertIn('FLP3FBN01ELBSX length=250 xy=1766_01', str(obs.content))
32+
33+
34+
class PushFileToCentralHandlerTests(OauthTestingBase):
35+
def setUp(self):
36+
super(PushFileToCentralHandlerTests, self).setUp()
37+
38+
def test_post(self):
39+
endpoint = '/cloud/push_file_to_central/'
40+
base_data_dir = qdb.util.get_db_files_base_dir()
41+
42+
# create a test file "locally", i.e. in current working directory
43+
fp_source = 'foo.bar'
44+
with open(fp_source, 'w') as f:
45+
f.write("this is a test\n")
46+
self._files_to_remove.append(fp_source)
47+
48+
# if successful, expected location of the file in BASE_DATA_DIR
49+
fp_target = base_data_dir + '/bar/' + basename(fp_source)
50+
self._files_to_remove.append(fp_target)
51+
52+
# create a second test file
53+
fp_source2 = 'foo_two.bar'
54+
with open(fp_source2, 'w') as f:
55+
f.write("this is another test\n")
56+
self._files_to_remove.append(fp_source2)
57+
fp_target2 = base_data_dir + '/barr/' + basename(fp_source2)
58+
self._files_to_remove.append(fp_target2)
59+
60+
# test raise error if no file is given
61+
obs = self.post_authed(endpoint)
62+
self.assertEqual(obs.reason, "No files to upload defined!")
63+
64+
# test correct mechanism
65+
with open(fp_source, 'rb') as fh:
66+
obs = self.post_authed(endpoint, files={'bar/': fh})
67+
self.assertIn('Stored 1 files into BASE_DATA_DIR of Qiita',
68+
str(obs.content))
69+
self.assertTrue(filecmp.cmp(fp_source, fp_target, shallow=False))
70+
71+
# check if error is raised, if file already exists
72+
with open(fp_source, 'rb') as fh:
73+
obs = self.post_authed(endpoint, files={'bar/': fh})
74+
self.assertIn("already present in Qiita's BASE_DATA_DIR!",
75+
obs.reason)
76+
77+
# test transfer of multiple files
78+
if exists(fp_target):
79+
remove(fp_target)
80+
with open(fp_source, 'rb') as fh1:
81+
with open(fp_source2, 'rb') as fh2:
82+
obs = self.post_authed(
83+
endpoint, files={'bar/': fh1, 'barr/': fh2})
84+
self.assertIn('Stored 2 files into BASE_DATA_DIR of Qiita',
85+
str(obs.content))
86+
self.assertTrue(filecmp.cmp(fp_source, fp_target,
87+
shallow=False))
88+
self.assertTrue(filecmp.cmp(fp_source2, fp_target2,
89+
shallow=False))
90+
91+
92+
if __name__ == "__main__":
93+
main()

qiita_pet/webserver.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
from qiita_pet.handlers.rest import ENDPOINTS as REST_ENDPOINTS
8686
from qiita_pet.handlers.qiita_redbiom import RedbiomPublicSearch
8787
from qiita_pet.handlers.public import PublicHandler
88+
from qiita_pet.handlers.cloud_handlers import ENDPOINTS as CLOUD_ENDPOINTS
8889

8990
if qiita_config.portal == "QIITA":
9091
from qiita_pet.handlers.portal import (
@@ -244,6 +245,11 @@ def __init__(self):
244245
(r"/qiita_db/studies/(.*)", APIStudiesListing)
245246
]
246247

248+
# expose endpoints necessary for https file communication between
249+
# master and plugins IF no shared file system for base_data_dir is
250+
# intended
251+
handlers.extend(CLOUD_ENDPOINTS)
252+
247253
# rest endpoints
248254
handlers.extend(REST_ENDPOINTS)
249255

0 commit comments

Comments
 (0)