Skip to content

Commit

Permalink
Merge pull request #100 from TheJacksonLaboratory/G3-491-update-genes…
Browse files Browse the repository at this point in the history
…et-endpoints-to-allow-public-responses

G3-491: Allow unauthenticated requests on public Geneset data.
  • Loading branch information
bergsalex authored Oct 30, 2024
2 parents abe2f1f + 817760b commit 452db5a
Show file tree
Hide file tree
Showing 7 changed files with 609 additions and 541 deletions.
1,003 changes: 524 additions & 479 deletions poetry.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "geneweaver-api"
version = "0.10.0"
version = "0.11.0a0"
description = "The Geneweaver API"
authors = [
"Alexander Berger <alexander.berger@jax.org>",
Expand All @@ -20,12 +20,13 @@ python = "^3.9"
geneweaver-core = "^0.10.0a3"
fastapi = {extras = ["all"], version = "^0.115.0"}
uvicorn = {extras = ["standard"], version = "^0.30.0"}
geneweaver-db = "^0.5.0"
geneweaver-db = "^0.6.0a1"
psycopg-pool = "^3.1.7"
requests = "^2.32.3"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
psycopg-binary = "3.1.18"
pydantic-settings = "^2.3.4"
jax-apiutils = "^0.1.0a6"

[tool.poetry.group.dev.dependencies]
geneweaver-testing = "^0.1.2"
Expand All @@ -45,4 +46,4 @@ build-backend = "poetry.core.masonry.api"

# TODO: Remove this when batch API is hooked up and tested.
[tool.coverage.report]
omit = ["*/batch.py"]
omit = ["*/batch.py"]
26 changes: 15 additions & 11 deletions src/geneweaver/api/controller/genesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
from tempfile import TemporaryDirectory
from typing import Optional, Set

from fastapi import APIRouter, Depends, HTTPException, Path, Query, Security
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, Security
from fastapi.responses import FileResponse, StreamingResponse
from geneweaver.api import dependencies as deps
from geneweaver.api.schemas.apimodels import GeneValueReturn, SearchResponse
from geneweaver.api.schemas.apimodels import GeneValueReturn
from geneweaver.api.schemas.auth import UserInternal
from geneweaver.api.schemas.search import GenesetSearch
from geneweaver.api.services import geneset as genset_service
from geneweaver.api.services import publications as publication_service
from geneweaver.core.enum import GeneIdentifier, GenesetTier, Species
from geneweaver.core.schema.score import GenesetScoreType, ScoreType
from geneweaver.db import search as db_search
from jax.apiutils import CollectionResponse
from typing_extensions import Annotated

from . import message as api_message
Expand Down Expand Up @@ -151,17 +152,20 @@ def get_visible_genesets(

@router.get("/search")
def search(
request: Request,
geneset_search: Annotated[GenesetSearch, Query()],
user: UserInternal = Security(deps.optional_full_user),
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
) -> SearchResponse:
) -> CollectionResponse:
"""Search genesets."""
return SearchResponse(
return CollectionResponse(
db_search.genesets(
cursor,
is_readable_by=0 if user is None else user.id,
**geneset_search.model_dump(exclude_none=True),
)
),
url=request.url_for("search"),
**geneset_search.model_dump(exclude_none=True),
)


Expand All @@ -170,7 +174,7 @@ def get_geneset(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
gene_id_type: Optional[GeneIdentifier] = None,
in_threshold: Optional[bool] = None,
Expand Down Expand Up @@ -199,7 +203,7 @@ def get_geneset_values(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
gene_id_type: Optional[GeneIdentifier] = None,
in_threshold: Optional[bool] = None,
Expand Down Expand Up @@ -232,7 +236,7 @@ def get_export_geneset_by_id_type(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
temp_dir: TemporaryDirectory = Depends(deps.get_temp_dir),
gene_id_type: Optional[GeneIdentifier] = None,
Expand Down Expand Up @@ -283,7 +287,7 @@ def get_geneset_metadata(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
include_pub_info: Optional[bool] = False,
) -> dict:
Expand All @@ -306,7 +310,7 @@ def get_publication_for_geneset(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
) -> dict:
"""Get the publication associated with the geneset."""
Expand Down Expand Up @@ -358,7 +362,7 @@ def get_geneset_ontology_terms(
geneset_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
user: deps.OptionalFullUserDep,
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
limit: Annotated[
Optional[int],
Expand Down
6 changes: 2 additions & 4 deletions src/geneweaver/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,14 @@ def _get_user_details(cursor: Cursor, user: UserInternal) -> UserInternal:
:param user: The user object.
"""
try:
user.id = db_user.by_sso_id_and_email(cursor, user.sso_id, user.email)[0][
"usr_id"
]
user.id = db_user.by_sso_id_and_email(cursor, user.sso_id, user.email).id
except IndexError as e:
if db_user.sso_id_exists(cursor, user.sso_id):
raise AuthenticationMismatch(
detail="Email and SSO ID Mismatch. Please contact and administrator."
) from e
elif db_user.email_exists(cursor, user.email):
user.id = db_user.by_email(cursor, user.email)[0]["usr_id"]
user.id = db_user.by_email(cursor, user.email).id
_ = db_user.link_user_id_with_sso_id(cursor, user.id, user.sso_id)
else:
if not user.name:
Expand Down
54 changes: 27 additions & 27 deletions src/geneweaver/api/services/geneset.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
ONTO_GSO_REF_TYPE = "GeneWeaver Primary Annotation"


def determine_user_id(user: Optional[User] = None) -> int:
"""Determine the user ID from the user object.
:param user: The user object.
:return: The user ID.
"""
if user is None or user.id is None:
return 0
return user.id


def determine_geneset_access(
user: Optional[User] = None,
curation_tier: Optional[Set[GenesetTier]] = None,
Expand All @@ -36,9 +47,10 @@ def determine_geneset_access(
is_readable_by = None

if user_is_none:
curation_tier = determine_public_geneset_curation_tier(curation_tier)
if only_my_genesets:
raise UnauthorizedException()
curation_tier = determine_public_geneset_curation_tier(curation_tier)
is_readable_by = 0
else:
is_readable_by = user.id
if only_my_genesets:
Expand Down Expand Up @@ -178,12 +190,9 @@ def get_geneset_metadata(
@return: dictionary response (geneset).
"""
try:
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

results = db_geneset.get(
cursor,
is_readable_by=user.id,
is_readable_by=determine_user_id(user),
gs_id=geneset_id,
with_publication_info=include_pub_info,
)
Expand All @@ -206,12 +215,9 @@ def get_geneset(
@return: dictionary response (geneset and genset values).
"""
try:
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

results = db_geneset.get(
cursor,
is_readable_by=user.id,
is_readable_by=determine_user_id(user),
gs_id=geneset_id,
with_publication_info=False,
)
Expand Down Expand Up @@ -247,14 +253,11 @@ def get_geneset_gene_values(
:return: dictionary response (geneset and genset values).
"""
try:
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

## Check genset exists and user can read it
results = db_geneset.get(
cursor,
gs_id=geneset_id,
is_readable_by=user.id,
is_readable_by=determine_user_id(user),
with_publication_info=False,
)
if len(results) <= 0:
Expand Down Expand Up @@ -307,12 +310,9 @@ def get_geneset_w_gene_id_type(
@return: Dictionary response (geneset identifier, geneset, and genset values).
"""
try:
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

results = db_geneset.get(
cursor,
is_readable_by=user.id,
is_readable_by=determine_user_id(user),
gs_id=geneset_id,
with_publication_info=False,
)
Expand Down Expand Up @@ -406,14 +406,17 @@ def update_geneset_threshold(
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

if not db_geneset.user_is_owner(
cursor=cursor, user_id=user.id, geneset_id=geneset_id
):
try:
db_threshold.set_geneset_threshold(
cursor=cursor,
user_id=user.id,
geneset_id=geneset_id,
geneset_score_type=geneset_score,
)

except ValueError:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

db_threshold.set_geneset_threshold(
cursor=cursor, geneset_id=geneset_id, geneset_score_type=geneset_score
)
return {}

except Exception as err:
Expand All @@ -438,11 +441,8 @@ def get_geneset_ontology_terms(
@return: dictionary response (ontology terms).
"""
try:
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}

is_gs_readable = db_geneset.is_readable(
cursor=cursor, user_id=user.id, geneset_id=geneset_id
cursor=cursor, user_id=determine_user_id(user), geneset_id=geneset_id
)
if is_gs_readable is False:
return {"error": True, "message": message.INACCESSIBLE_OR_FORBIDDEN}
Expand Down
52 changes: 36 additions & 16 deletions tests/services/test_genset.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ def test_get_geneset(mock_db_geneset, mock_db_genset_value):
@patch("geneweaver.api.services.geneset.db_geneset")
def test_get_geneset_no_user_access(mock_db_geneset):
"""Test get geneset by ID with no user access."""
mock_db_geneset.get.return_value = []
response = geneset.get_geneset(None, 1234, None)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert "data" in response
assert response["data"] is None

mock_db_geneset.get.return_value = []
response = geneset.get_geneset(None, 1234, mock_user)
assert response.get("data") is None
assert "data" in response
assert response["data"] is None


@patch("geneweaver.api.services.geneset.db_geneset")
Expand Down Expand Up @@ -126,17 +128,28 @@ def test_get_geneset_w_gene_id_type_2_response(
)


def test_get_geneset_w_gene_id_type_no_user():
@patch("geneweaver.api.services.geneset.db_geneset")
@patch("geneweaver.api.services.geneset.get_gsv_w_gene_homology_update")
def test_get_geneset_w_gene_id_type_no_user(mock_db_geneset, mock_gsv):
"""Test get_geneset_w_gene_id_type with invalid user."""
mock_db_geneset.get.return_value = [geneset_by_id_resp.get("geneset")]
mock_gsv.return_value = geneset_by_id_resp.get("geneset_values")

response = geneset.get_geneset_w_gene_id_type(None, 1234, None, GeneIdentifier(2))
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert response is not None
assert isinstance(response, dict)
assert "gene_identifier_type" in response
assert "geneset" in response
assert "geneset_values" in response

response = geneset.get_geneset_w_gene_id_type(
None, 1234, User(id=None), GeneIdentifier(2)
)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert response is not None
assert isinstance(response, dict)
assert "gene_identifier_type" in response
assert "geneset" in response
assert "geneset_values" in response


@patch("geneweaver.api.services.geneset.db_geneset")
Expand All @@ -152,17 +165,18 @@ def test_geneset_w_gene_id_type_db_call_error(mock_db_geneset):
def test_get_geneset_metadata(mock_db_geneset):
"""Test get geneset metadata by geneset id."""
mock_db_geneset.get.return_value = [geneset_by_id_resp.get("geneset")]
response = geneset.get_geneset_metadata(None, 1234, mock_user)

response = geneset.get_geneset_metadata(None, 1234, mock_user)
assert "geneset" in response
assert response.get("geneset") == geneset_by_id_resp["geneset"]

response = geneset.get_geneset_metadata(None, 1234, None)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert "geneset" in response
assert response.get("geneset") == geneset_by_id_resp["geneset"]

response = geneset.get_geneset_metadata(None, 1234, User(id=None))
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert "geneset" in response
assert response.get("geneset") == geneset_by_id_resp["geneset"]


@patch("geneweaver.api.services.geneset.db_geneset")
Expand Down Expand Up @@ -378,14 +392,18 @@ def test_get_geneset_gene_values_db_errors(mock_db_geneset_value):
)


@patch("geneweaver.api.services.geneset.db_geneset.get")
@patch("geneweaver.api.services.geneset.db_geneset_value")
def test_get_geneset_gene_values_invalid_user(mock_db_geneset_value):
def test_get_geneset_gene_values_invalid_user(
mock_db_geneset_get, mock_db_geneset_value
):
"""Test invalid user."""
mock_db_geneset_get.return_value = []
response = geneset.get_geneset_gene_values(
None, user=None, geneset_id=1234, gene_id_type=None
)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN
assert "data" in response
assert response["data"] is None


@patch("geneweaver.api.services.geneset.db_geneset")
Expand All @@ -411,13 +429,15 @@ def test_geneset_thershold_update_errors(mock_db_threshold, mock_db_geneset):
geneset_threshold = GenesetScoreType(**geneset_threshold_update_req)

# user is not the geneset owner
mock_db_threshold.set_geneset_threshold.side_effect = ValueError()
response = geneset.update_geneset_threshold(
cursor=None, user=mock_user, geneset_id=1234, geneset_score=geneset_threshold
)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN

# user is not logged-in
mock_db_threshold.set_geneset_threshold.side_effect = ValueError()
response = geneset.update_geneset_threshold(
cursor=None, user=None, geneset_id=1234, geneset_score=geneset_threshold
)
Expand Down
2 changes: 1 addition & 1 deletion tests/services/test_ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def test_get_geneset_ontology_terms_no_user(mock_db_geneset, mock_db_ontology):

response = geneset.get_geneset_ontology_terms(None, 1234, None)

assert response == {"error": True, "message": message.ACCESS_FORBIDDEN}
assert response == {"error": True, "message": message.INACCESSIBLE_OR_FORBIDDEN}


@patch("geneweaver.api.services.geneset.db_ontology")
Expand Down

0 comments on commit 452db5a

Please sign in to comment.