Skip to content

Commit

Permalink
Merge pull request #779 from microbiomedata/update_password
Browse files Browse the repository at this point in the history
Implement an API endpoint an admin can use to update a user's password
  • Loading branch information
eecavanna authored Dec 6, 2024
2 parents 76c87ff + 1918301 commit 17a8dd8
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 6 deletions.
2 changes: 1 addition & 1 deletion nmdc_runtime/api/core/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def __init__(
bearer_creds: Optional[HTTPAuthorizationCredentials] = Depends(
bearer_credentials
),
grant_type: str = Form(None, regex="^password$|^client_credentials$"),
grant_type: str = Form(None, pattern="^password$|^client_credentials$"),
username: Optional[str] = Form(None),
password: Optional[str] = Form(None),
scope: str = Form(""),
Expand Down
47 changes: 45 additions & 2 deletions nmdc_runtime/api/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ async def login_for_access_token(
}


@router.get("/users/me", response_model=User, response_model_exclude_unset=True)
@router.get(
"/users/me",
response_model=User,
response_model_exclude_unset=True,
name="Get User Information",
description="Get information about the logged-in user",
)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user

Expand All @@ -156,7 +162,13 @@ def check_can_create_user(requester: User):
)


@router.post("/users", status_code=status.HTTP_201_CREATED, response_model=User)
@router.post(
"/users",
status_code=status.HTTP_201_CREATED,
response_model=User,
name="Create User",
description="Create new user (Admin Only)",
)
def create_user(
user_in: UserIn,
requester: User = Depends(get_current_active_user),
Expand All @@ -170,3 +182,34 @@ def create_user(
).model_dump(exclude_unset=True)
)
return mdb.users.find_one({"username": user_in.username})


@router.put(
"/users",
status_code=status.HTTP_200_OK,
response_model=User,
name="Update User",
description="Update information about the user having the specified username (Admin Only)",
)
def update_user(
user_in: UserIn,
requester: User = Depends(get_current_active_user),
mdb: pymongo.database.Database = Depends(get_mongo_db),
):
check_can_create_user(requester)
username = user_in.username

if user_in.password:
user_dict = UserInDB(
**user_in.model_dump(),
hashed_password=get_password_hash(
user_in.password
), # Store the password hash
).model_dump(exclude_unset=True)
else:
user_dict = UserIn(
**user_in.model_dump(),
).model_dump(exclude_unset=True)

mdb.users.update_one({"username": username}, {"$set": user_dict})
return mdb.users.find_one({"username": user_in.username})
75 changes: 72 additions & 3 deletions tests/test_api/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import requests
from dagster import build_op_context
from starlette import status
from tenacity import wait_random_exponential, retry
from tenacity import wait_random_exponential, stop_after_attempt, retry
from toolz import get_in

from nmdc_runtime.api.core.auth import get_password_hash
Expand Down Expand Up @@ -140,13 +140,12 @@ def test_update_operation():
)


@pytest.mark.skip(reason="Skipping because test causes suite to hang")
def test_create_user():
mdb = get_mongo(run_config_frozen__normal_env).db
rs = ensure_test_resources(mdb)
base_url = os.getenv("API_HOST")

@retry(wait=wait_random_exponential(multiplier=1, max=60))
@retry(wait=wait_random_exponential(multiplier=1, max=60), stop=stop_after_attempt(3))
def get_token():
"""
Expand Down Expand Up @@ -192,6 +191,76 @@ def get_token():
)


def test_update_user():
mdb = get_mongo(run_config_frozen__normal_env).db
rs = ensure_test_resources(mdb)
base_url = os.getenv("API_HOST")

# Try up to three times, waiting for up to 60 seconds between each attempt.
@retry(wait=wait_random_exponential(multiplier=1, max=60), stop=stop_after_attempt(3))
def get_token():
"""
Fetch an auth token from the Runtime API.
"""

_rv = requests.post(
base_url + "/token",
data={
"grant_type": "password",
"username": rs["user"]["username"],
"password": rs["user"]["password"],
},
)
token_response = _rv.json()
return token_response["access_token"]

headers = {"Authorization": f"Bearer {get_token()}"}

user_in1 = UserIn(username="foo", password="oldpass")
mdb.users.delete_one({"username": user_in1.username})
mdb.users.update_one(
{"username": rs["user"]["username"]},
{"$addToSet": {"site_admin": "nmdc-runtime-useradmin"}},
)
rv_create = requests.request(
"POST",
url=(base_url + "/users"),
headers=headers,
json=user_in1.model_dump(exclude_unset=True),
)

u1 = mdb.users.find_one({"username": user_in1.username})

user_in2 = UserIn(username="foo", password="newpass")

rv_update = requests.request(
"PUT",
url=(base_url + "/users"),
headers=headers,
json=user_in2.model_dump(exclude_unset=True),
)


u2 = mdb.users.find_one({"username": user_in2.username})
try:
assert rv_create.status_code == status.HTTP_201_CREATED
assert rv_update.status_code == status.HTTP_200_OK
assert u1['hashed_password'] != u2['hashed_password']

finally:
mdb.users.delete_one({"username": user_in1.username})
mdb.users.update_one(
{"username": rs["user"]["username"]},
{"$pull": {"site_admin": "nmdc-runtime-useradmin"}},
)

@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"])


def test_metadata_validate_json_0(api_site_client):
rv = api_site_client.request(
"POST",
Expand Down

0 comments on commit 17a8dd8

Please sign in to comment.