mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-09-16 13:27:59 +08:00
* Updates due dependency version changes. h5py recently changes and now values once returned as str are now returned as bytes. This would have caused a much larger change, so instead the version is restricted to <3.0.0. This caused the bulk of the testing failues. A few other changes were needed to make a few other tests pass. #1959
222 lines
9.4 KiB
Python
222 lines
9.4 KiB
Python
import unittest
|
|
import random
|
|
import time
|
|
import base64
|
|
import json
|
|
import requests
|
|
|
|
from flask import Flask, jsonify, make_response, request, redirect
|
|
from multiprocessing import Process
|
|
|
|
import jose
|
|
from server.common.config.app_config import AppConfig
|
|
from server.test import FIXTURES_ROOT, test_server
|
|
|
|
# This tests the oauth authentication type.
|
|
# This test starts a cellxgene server and a mock oauth server.
|
|
# API requests to login and logout and get the userinfo are made
|
|
# to the cellxgene server, which then sends requests to the mock
|
|
# oauth server.
|
|
|
|
# number of seconds that the oauth token is valid
|
|
TOKEN_EXPIRES = 2
|
|
|
|
# Create a mocked out oauth token, which servers all the endpoints needed by the oauth type.
|
|
mock_oauth_app = Flask("mock_oauth_app")
|
|
|
|
|
|
@mock_oauth_app.route("/authorize")
|
|
def authorize():
|
|
callback = request.args.get("redirect_uri")
|
|
state = request.args.get("state")
|
|
return redirect(callback + f"?code=fakecode&state={state}")
|
|
|
|
|
|
@mock_oauth_app.route("/oauth/token", methods=["POST"])
|
|
def token():
|
|
now = time.time()
|
|
expires_at = now + TOKEN_EXPIRES
|
|
headers = dict(alg="RS256", kid="fake_kid")
|
|
payload = dict(name="fake_user", sub="fake_id", email="fake_user@email.com", email_verified=True, exp=expires_at)
|
|
jwt = jose.jwt.encode(claims=payload, key="mysecret", algorithm="HS256", headers=headers)
|
|
r = {
|
|
"access_token": f"access-{now}",
|
|
"id_token": jwt,
|
|
"refresh_token": f"random-{now}",
|
|
"scope": "openid profile email",
|
|
"expires_in": TOKEN_EXPIRES,
|
|
"token_type": "Bearer",
|
|
"expires_at": expires_at,
|
|
}
|
|
return make_response(jsonify(r))
|
|
|
|
|
|
@mock_oauth_app.route("/v2/logout")
|
|
def logout():
|
|
return_to = request.args.get("returnTo")
|
|
return redirect(return_to)
|
|
|
|
|
|
@mock_oauth_app.route("/.well-known/jwks.json")
|
|
def jwks():
|
|
data = dict(alg="RS256", kty="RSA", use="sig", kid="fake_kid",)
|
|
return make_response(jsonify(dict(keys=[data])))
|
|
|
|
|
|
# function to launch the mock oauth server
|
|
def launch_mock_oauth(mock_port):
|
|
mock_oauth_app.run(port=mock_port)
|
|
|
|
|
|
class AuthTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
# The port that the mock oauth server will listen on
|
|
cls.mock_port = random.randint(10000, 12000)
|
|
cls.dataset_dataroot = FIXTURES_ROOT
|
|
cls.mock_oauth_process = Process(target=launch_mock_oauth, args=(cls.mock_port,))
|
|
cls.mock_oauth_process.start()
|
|
|
|
# Verify that the mock oauth server is ready (accepting requests) before starting the tests.
|
|
|
|
# The following lines are polling until the mock server is ready.
|
|
# The issue is we are starting a mock oauth server, then we are starting a cellxgene server,
|
|
# which will start making requests to the mock oauth server.
|
|
# So there is a race condition because the mock oauth server needs to be ready before it gets requests.
|
|
# We check to see if it is ready, and if not we wait 1 second, then try again.
|
|
# If it gets to 5 seconds, which is shouldn't, we assume something has gone wrong and fail the test.
|
|
server_okay = False
|
|
for _ in range(5):
|
|
try:
|
|
response = requests.get(f"http://localhost:{cls.mock_port}/.well-known/jwks.json")
|
|
if response.status_code == 200:
|
|
server_okay = True
|
|
break
|
|
except: # noqa: E722
|
|
pass
|
|
|
|
# wait one second and try again
|
|
time.sleep(1)
|
|
|
|
assert(server_okay)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.mock_oauth_process.terminate()
|
|
|
|
def auth_flow(self, app_config, cookie_key=None):
|
|
|
|
app_config.update_server_config(
|
|
app__api_base_url="local",
|
|
authentication__type="oauth",
|
|
authentication__params_oauth__oauth_api_base_url=f"http://localhost:{self.mock_port}",
|
|
authentication__params_oauth__client_id="mock_client_id",
|
|
authentication__params_oauth__client_secret="mock_client_secret",
|
|
authentication__params_oauth__jwt_decode_options={"verify_signature": False, "verify_iss": False},
|
|
)
|
|
|
|
app_config.update_server_config(multi_dataset__dataroot=self.dataset_dataroot)
|
|
app_config.complete_config()
|
|
|
|
with test_server(app_config=app_config) as server:
|
|
session = requests.Session()
|
|
|
|
# auth datasets
|
|
config = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/config").json()
|
|
userinfo = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo").json()
|
|
|
|
self.assertFalse(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertIsNone(userinfo["userinfo"]["username"])
|
|
self.assertTrue(config["config"]["authentication"]["requires_client_login"])
|
|
self.assertTrue(config["config"]["parameters"]["annotations"])
|
|
|
|
login_uri = config["config"]["authentication"]["login"]
|
|
logout_uri = config["config"]["authentication"]["logout"]
|
|
|
|
self.assertEqual(login_uri, f"{server}/login?dataset=d/pbmc3k.cxg/")
|
|
self.assertEqual(logout_uri, f"{server}/logout?dataset=d/pbmc3k.cxg/")
|
|
|
|
r = session.get(login_uri)
|
|
# check that the login redirect worked
|
|
self.assertEqual(r.history[0].status_code, 302)
|
|
self.assertEqual(r.url, f"{server}/d/pbmc3k.cxg/")
|
|
config = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/config").json()
|
|
userinfo = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo").json()
|
|
self.assertTrue(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertEqual(userinfo["userinfo"]["username"], "fake_user")
|
|
self.assertEqual(userinfo["userinfo"]["email"], "fake_user@email.com")
|
|
self.assertTrue(config["config"]["parameters"]["annotations"])
|
|
|
|
if cookie_key:
|
|
cookie = session.cookies.get(cookie_key)
|
|
token = json.loads(base64.b64decode(cookie))
|
|
access_token_before = token.get("access_token")
|
|
id_token_before = token.get("id_token")
|
|
|
|
# let the token expire
|
|
time.sleep(TOKEN_EXPIRES + 1)
|
|
|
|
# check that refresh works
|
|
session.get(login_uri)
|
|
userinfo = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo").json()
|
|
self.assertTrue(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertEqual(userinfo["userinfo"]["username"], "fake_user")
|
|
|
|
cookie = session.cookies.get(cookie_key)
|
|
token = json.loads(base64.b64decode(cookie))
|
|
access_token_after = token.get("access_token")
|
|
id_token_after = token.get("id_token")
|
|
|
|
self.assertNotEqual(access_token_before, access_token_after)
|
|
self.assertNotEqual(id_token_before, id_token_after)
|
|
|
|
# invalid cookie is rejected
|
|
session.cookies.set(cookie_key, "TEST_" + cookie)
|
|
self.assertTrue(cookie_key in session.cookies)
|
|
response = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo")
|
|
# this is not an error, the invalid cookie is just ignored.
|
|
self.assertEqual(response.status_code, 200)
|
|
userinfo = response.json()
|
|
self.assertFalse(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertIsNone(userinfo["userinfo"]["username"])
|
|
|
|
# invalid id_token is rejected
|
|
test_token = token
|
|
test_token["id_token"] = "TEST_" + id_token_after
|
|
encoded_cookie = base64.b64encode(json.dumps(test_token).encode()).decode()
|
|
session.cookies.set(cookie_key, encoded_cookie)
|
|
response = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo")
|
|
# this is not an error, the invalid id_token is just ignored.
|
|
self.assertEqual(response.status_code, 200)
|
|
userinfo = response.json()
|
|
self.assertFalse(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertIsNone(userinfo["userinfo"]["username"])
|
|
|
|
r = session.get(logout_uri)
|
|
# check that the logout redirect worked
|
|
self.assertEqual(r.history[0].status_code, 302)
|
|
self.assertEqual(r.url, f"{server}/d/pbmc3k.cxg/")
|
|
config = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/config").json()
|
|
userinfo = session.get(f"{server}/d/pbmc3k.cxg/api/v0.2/userinfo").json()
|
|
self.assertFalse(userinfo["userinfo"]["is_authenticated"])
|
|
self.assertIsNone(userinfo["userinfo"]["username"])
|
|
self.assertTrue(config["config"]["parameters"]["annotations"])
|
|
|
|
def test_auth_oauth_session(self):
|
|
# test with session cookies
|
|
app_config = AppConfig()
|
|
app_config.update_server_config(app__flask_secret_key="secret")
|
|
app_config.update_server_config(authentication__params_oauth__session_cookie=True,)
|
|
self.auth_flow(app_config)
|
|
|
|
def test_auth_oauth_cookie(self):
|
|
# test with specified cookie
|
|
app_config = AppConfig()
|
|
app_config.update_server_config(app__flask_secret_key="secret")
|
|
app_config.update_server_config(
|
|
authentication__params_oauth__session_cookie=False,
|
|
authentication__params_oauth__cookie=dict(key="test_cxguser", httponly=True, max_age=60),
|
|
)
|
|
|
|
self.auth_flow(app_config, "test_cxguser")
|