-
Notifications
You must be signed in to change notification settings - Fork 0
AAI-182 refactor and clean up user/role checking to enable admin endpoints #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
c62bd6d
Define admin roles in config
marius-mather 8c9eaee
Update .env example with admin_roles
marius-mather cc2a151
Add a user_is_admin dependency for checking admin roles
marius-mather 1a2d5cd
Fix roles claim name: we now prefix with HTTPS (as recommended by Auth0)
marius-mather 7971073
Remove old admin check
marius-mather e88afdc
Update AccessTokenPayload so it's easier to specify roles
marius-mather 1ef747b
Update User.is_admin() to check configured roles
marius-mather 243d548
Add a fixture for acting as an admin
marius-mather 7dc7578
Add admin_roles to mock settings
marius-mather 8800f50
Rename client_with_settings_override: too clunky for something we use…
marius-mather 5d4530b
Add data generation for Auth0 user API data
marius-mather ffdbbdb
Move user schema tests to test_user_schema
marius-mather f72c89a
Initial Auth0 client for making requests to the management API
marius-mather 0c22725
Initial admin router with one route defined (and all routes protected…
marius-mather be557e1
Add admin router to app
marius-mather 8e6de4d
Add tests for admin endpoints
marius-mather 56a074d
Force .env file to be ignored in tests
marius-mather 9b984d7
Don't load env variables in main, just grab the needed value from the…
marius-mather 184bfa3
Rework how we force the env file to be ignored in tests
marius-mather bb08277
Update tests to use the test client
marius-mather 99bbaca
Style fix
marius-mather File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| __all__ = ["Auth0Client"] | ||
|
|
||
| import httpx | ||
|
|
||
| from auth0.schemas import Auth0UserResponse | ||
|
|
||
|
|
||
| class Auth0Client: | ||
|
|
||
| def __init__(self, domain: str): | ||
| self.domain = domain | ||
|
|
||
| def get_users(self, access_token: str) -> list[Auth0UserResponse]: | ||
| url = f"https://{self.domain}/api/v2/users" | ||
| headers = {"Authorization": f"Bearer {access_token}"} | ||
| resp = httpx.get(url, headers=headers) | ||
| return resp.json() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| from datetime import datetime | ||
| from typing import List, Optional | ||
|
|
||
| from pydantic import BaseModel, ConfigDict, EmailStr | ||
|
|
||
|
|
||
| class Auth0UserResponse(BaseModel): | ||
| """ | ||
| Response returned by Auth0's /users endpoint. | ||
| Note we have our own Auth0User model | ||
| that includes specifying the metadata fields we use. | ||
| """ | ||
| user_id: str | ||
| email: EmailStr | ||
| email_verified: bool | ||
| username: Optional[str] = None | ||
| phone_number: Optional[str] = None | ||
| phone_verified: Optional[bool] = None | ||
| created_at: datetime | ||
| updated_at: datetime | ||
| identities: List[dict] | ||
| app_metadata: Optional[dict] = None | ||
| user_metadata: Optional[dict] = None | ||
| picture: Optional[str] = None | ||
| name: Optional[str] = None | ||
| nickname: Optional[str] = None | ||
| last_ip: Optional[str] = None | ||
| last_login: Optional[datetime] = None | ||
| logins_count: Optional[int] = None | ||
| blocked: Optional[bool] = None | ||
| given_name: Optional[str] = None | ||
| family_name: Optional[str] = None | ||
|
|
||
| model_config = ConfigDict(extra="allow") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from fastapi import APIRouter, Depends | ||
|
|
||
| from auth.config import Settings, get_settings | ||
| from auth.management import get_management_token | ||
| from auth.validator import user_is_admin | ||
| from auth0.client import Auth0Client | ||
| from auth0.schemas import Auth0UserResponse | ||
|
|
||
| router = APIRouter(prefix="/admin", tags=["admin"], | ||
| dependencies=[Depends(user_is_admin)]) | ||
|
|
||
|
|
||
| def get_auth0_client(settings: Settings = Depends(get_settings)): | ||
| return Auth0Client(settings.auth0_domain) | ||
|
|
||
|
|
||
| @router.get("/users", | ||
| response_model=list[Auth0UserResponse]) | ||
| def get_users(settings: Settings = Depends(get_settings), | ||
| client: Auth0Client = Depends(get_auth0_client)): | ||
| token = get_management_token(settings=settings) | ||
| resp = client.get_users(token) | ||
| return resp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import pytest | ||
| from fastapi import HTTPException | ||
|
|
||
| from auth.validator import get_current_user, user_is_admin | ||
| from main import app | ||
| from tests.datagen import ( | ||
| AccessTokenPayloadFactory, | ||
| Auth0UserResponseFactory, | ||
| UserFactory, | ||
| ) | ||
|
|
||
|
|
||
| def test_get_users_requires_admin_unauthorized(test_client, mocker): | ||
| def get_nonadmin_user(): | ||
| payload = AccessTokenPayloadFactory.build(biocommons_roles=["User"]) | ||
| return UserFactory.build(access_token=payload) | ||
|
|
||
| app.dependency_overrides[get_current_user] = get_nonadmin_user | ||
| mocker.patch("routers.admin.get_management_token", return_value="mock_token") | ||
| resp = test_client.get("/admin/users") | ||
| assert resp.status_code == 403 | ||
| assert resp.json() == {"detail": "You must be an admin to access this endpoint."} | ||
| app.dependency_overrides.clear() | ||
|
|
||
|
|
||
| def test_user_is_admin(mock_settings): | ||
| payload = AccessTokenPayloadFactory.build(biocommons_roles=["Admin"]) | ||
| admin_user = UserFactory.build(access_token=payload) | ||
| assert user_is_admin(current_user=admin_user, settings=mock_settings) | ||
|
|
||
|
|
||
| def test_user_is_admin_nonadmin_user(mock_settings): | ||
| payload = AccessTokenPayloadFactory.build(biocommons_roles=["User"]) | ||
| user = UserFactory.build(access_token=payload) | ||
| with pytest.raises(HTTPException, match="You must be an admin to access this endpoint."): | ||
| user_is_admin(current_user=user, settings=mock_settings) | ||
|
|
||
|
|
||
| def test_get_users(mocker, test_client, as_admin_user): | ||
| mocker.patch("routers.admin.get_management_token", return_value="mock_token") | ||
| mock_client = mocker.patch("routers.admin.Auth0Client") | ||
| users = Auth0UserResponseFactory.batch(3) | ||
| mock_client().get_users.return_value = users | ||
| resp = test_client.get("/admin/users") | ||
| assert resp.status_code == 200 | ||
| assert len(resp.json()) == 3 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.