Skip to content

Commit

Permalink
Workflow queries:run access for site_clients (#783)
Browse files Browse the repository at this point in the history
* Initial stab at pyproject.toml

* Add support to extract username from a "client:" token

* remove pyproject.toml (this should be a separate PR)

* switch or to and

* coerce client into user object

* Modularize client user code

* Add test for queries:find including site client functionality

* clean up prints

* style: reformat

* linting and reformatting

* switch to pytest.raises

* Update comments in test

* chore: black

* chore: tidy

* fix: idempotent on rerun

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Donny Winston <donny@polyneme.xyz>
  • Loading branch information
3 people authored Nov 21, 2024
1 parent 8013f3f commit 2eb01b0
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 19 deletions.
34 changes: 30 additions & 4 deletions nmdc_runtime/api/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
TokenData,
bearer_scheme,
)

from nmdc_runtime.api.models.site import get_site

from nmdc_runtime.api.db.mongo import get_mongo_db


Expand Down Expand Up @@ -60,21 +63,44 @@ async def get_current_user(
subject: str = payload.get("sub")
if subject is None:
raise credentials_exception
if not subject.startswith("user:"):
if not subject.startswith("user:") and not subject.startswith("client:"):
raise credentials_exception
username = subject.split("user:", 1)[1]

# subject is in the form "user:foo" or "client:bar"
username = subject.split(":", 1)[1]
token_data = TokenData(subject=username)
except (JWTError, AttributeError) as e:
print(f"jwt error: {e}")
raise credentials_exception
user = get_user(mdb, username=token_data.subject)

# Coerce a "client" into a "user"
# TODO: consolidate the client/user distinction.
if subject.startswith("user:"):
user = get_user(mdb, username=token_data.subject)
elif subject.startswith("client:"):
# construct a user from the client_id
user = get_client_user(mdb, client_id=token_data.subject)
else:
raise credentials_exception
if user is None:
raise credentials_exception
return user


def get_client_user(mdb, client_id: str) -> UserInDB:
site = get_site(mdb, client_id)
if site is None:
raise credentials_exception
client = next(client for client in site.clients if client.id == client_id)
if client is None:
raise credentials_exception
# Coerce the client into a user
user = UserInDB(username=client.id, hashed_password=client.hashed_secret)
return user


async def get_current_active_user(
current_user: User = Depends(get_current_user),
current_user: UserInDB = Depends(get_current_user),
) -> UserInDB:
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
Expand Down
160 changes: 145 additions & 15 deletions tests/test_api/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ def ensure_test_resources(mdb):
}


@pytest.fixture
def api_site_client():
mdb = get_mongo_db()
rs = ensure_test_resources(mdb)
return RuntimeApiSiteClient(base_url=os.getenv("API_HOST"), **rs["site_client"])


@pytest.fixture
def api_user_client():
mdb = get_mongo_db()
rs = ensure_test_resources(mdb)
return RuntimeApiUserClient(base_url=os.getenv("API_HOST"), **rs["user"])


@pytest.mark.skip(reason="Skipping because test causes suite to hang")
def test_update_operation():
mdb = get_mongo(run_config_frozen__normal_env).db
Expand Down Expand Up @@ -178,20 +192,6 @@ def get_token():
)


@pytest.fixture
def api_site_client():
mdb = get_mongo_db()
rs = ensure_test_resources(mdb)
return RuntimeApiSiteClient(base_url=os.getenv("API_HOST"), **rs["site_client"])


@pytest.fixture
def api_user_client():
mdb = get_mongo_db()
rs = ensure_test_resources(mdb)
return RuntimeApiUserClient(base_url=os.getenv("API_HOST"), **rs["user"])


def test_metadata_validate_json_0(api_site_client):
rv = api_site_client.request(
"POST",
Expand Down Expand Up @@ -433,7 +433,137 @@ def test_find_planned_process_by_id(api_site_client):
)


def test_queries_run_update(api_user_client):
def test_run_query_find_user(api_user_client):
mdb = get_mongo_db()
if not mdb.biosample_set.find_one({"id": "nmdc:bsm-12-7mysck21"}):
mdb.biosample_set.insert_one(
json.loads(
(
REPO_ROOT_DIR / "tests" / "files" / "nmdc_bsm-12-7mysck21.json"
).read_text()
)
)

# Make sure user client works
response = api_user_client.request(
"POST",
"/queries:run",
{"find": "biosample_set", "filter": {"id": "nmdc:bsm-12-7mysck21"}},
)
assert response.status_code == 200
assert "cursor" in response.json()


def test_run_query_find_site(api_site_client):
mdb = get_mongo_db()
if not mdb.biosample_set.find_one({"id": "nmdc:bsm-12-7mysck21"}):
mdb.biosample_set.insert_one(
json.loads(
(
REPO_ROOT_DIR / "tests" / "files" / "nmdc_bsm-12-7mysck21.json"
).read_text()
)
)

# Make sure site client works
response = api_site_client.request(
"POST",
"/queries:run",
{"find": "biosample_set", "filter": {"id": "nmdc:bsm-12-7mysck21"}},
)
assert response.status_code == 200
assert "cursor" in response.json()


def test_run_query_delete(api_user_client):
mdb = get_mongo_db()
biosample_id = "nmdc:bsm-12-deleteme"

if not mdb.biosample_set.find_one({"id": biosample_id}):
mdb.biosample_set.insert_one({"id": biosample_id})

# Access should not work without permissions
mdb["_runtime"].api.allow.delete_many(
{
"username": api_user_client.username,
"action": "/queries:run(query_cmd:DeleteCommand)",
}
)
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
response = api_user_client.request(
"POST",
"/queries:run",
{
"delete": "biosample_set",
"deletes": [{"q": {"id": biosample_id}, "limit": 1}],
},
)
assert excinfo.value.response.status_code == 403

# Add persmissions to DB
mdb["_runtime"].api.allow.insert_one(
{
"username": api_user_client.username,
"action": "/queries:run(query_cmd:DeleteCommand)",
}
)
try:
response = api_user_client.request(
"POST",
"/queries:run",
{
"delete": "biosample_set",
"deletes": [{"q": {"id": biosample_id}, "limit": 1}],
},
)
assert response.status_code == 200
assert response.json()["n"] == 1
finally:
mdb["_runtime"].api.allow.delete_one({"username": api_user_client.username})


def test_run_query_delete_site(api_site_client):
mdb = get_mongo_db()
biosample_id = "nmdc:bsm-12-deleteme"

if not mdb.biosample_set.find_one({"id": biosample_id}):
mdb.biosample_set.insert_one({"id": biosample_id})

# Access should not work without permissions
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
response = api_site_client.request(
"POST",
"/queries:run",
{
"delete": "biosample_set",
"deletes": [{"q": {"id": biosample_id}, "limit": 1}],
},
)
assert excinfo.value.response.status_code == 403

# Add persmissions to DB
mdb["_runtime"].api.allow.insert_one(
{
"username": api_site_client.client_id,
"action": "/queries:run(query_cmd:DeleteCommand)",
}
)
try:
response = api_site_client.request(
"POST",
"/queries:run",
{
"delete": "biosample_set",
"deletes": [{"q": {"id": biosample_id}, "limit": 1}],
},
)
assert response.status_code == 200
assert response.json()["n"] == 1
finally:
mdb["_runtime"].api.allow.delete_one({"username": api_site_client.client_id})


def test_run_query_update(api_user_client):
"""Submit a request to store data that does not comply with the schema."""
mdb = get_mongo_db()
allow_spec = {
Expand Down

0 comments on commit 2eb01b0

Please sign in to comment.