Skip to content

Commit

Permalink
Refactor get_user_profile to read_user for consistency in method …
Browse files Browse the repository at this point in the history
…names

Remove `handle_update_user_request` and use `update_user` directly to consolidate logic
Add method to read `user_id` from a token for user update endpoint support
Add support for admin authentication to `update_user` endpoint
Refactor internal `_query_users_api` method to accept CRUD request objects
  • Loading branch information
NeonDaniel committed Nov 9, 2024
1 parent bd3c7a2 commit 542db86
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 67 deletions.
8 changes: 5 additions & 3 deletions neon_hana/app/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@
@user_route.post("/get")
async def get_user(request: GetUserRequest,
token: str = Depends(jwt_bearer)) -> User:
return mq_connector.get_user_profile(access_token=token, **dict(request))
user_id = jwt_bearer.client_manager.get_token_user_id(token)
return mq_connector.read_user(access_token=token, auth_user=user_id,
**dict(request))


@user_route.post("/update")
async def update_user(request: UpdateUserRequest,
token: str = Depends(jwt_bearer)) -> User:
return mq_connector.handle_update_user_request(access_token=token,
**dict(request))
return mq_connector.update_user(access_token=token,
**dict(request))
17 changes: 14 additions & 3 deletions neon_hana/auth/client_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def check_auth_request(self, client_id: str, username: str,
# permissions=_DEFAULT_USER_PERMISSIONS)
# user.permissions.node = AccessRoles.USER
else:
user = self._mq_connector.get_user_profile(username, password)
user = self._mq_connector.read_user(username, password)

create_time = round(time())
encode_data = {"client_id": client_id,
Expand Down Expand Up @@ -289,8 +289,8 @@ def check_refresh_request(self, access_token: Optional[str],
"permissions": PermissionsConfig.from_roles(refresh_data.roles)
}
if self._mq_connector:
user = self._mq_connector.get_user_profile(username=refresh_data.sub,
access_token=refresh_token)
user = self._mq_connector.read_user(username=refresh_data.sub,
access_token=refresh_token)
if not user.password_hash:
# This should not be possible, but don't let an error in the
# users service allow for injecting a new valid token to the db
Expand Down Expand Up @@ -334,6 +334,17 @@ def get_client_id(self, token: str) -> str:
self._jwt_algo))
return auth.client_id

def get_token_user_id(self, token: str) -> str:
"""
Extract the user_id from a JWT string
@param token: JWT to parse
@retrun: user_id associated with token
"""
auth = HanaToken(**jwt.decode(token, self._access_secret,
self._jwt_algo))
return auth.user_id


def validate_auth(self, token: str, origin_ip: str) -> bool:
ratelimit_id = f"{origin_ip}-total"
if not self.rate_limiter.get_all_buckets(ratelimit_id):
Expand Down
108 changes: 48 additions & 60 deletions neon_hana/mq_service_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from uuid import uuid4
from fastapi import HTTPException

from neon_data_models.models.api import CreateUserRequest, ReadUserRequest, \
UpdateUserRequest, DeleteUserRequest
from neon_mq_connector.utils.client_utils import send_mq_request
from neon_data_models.models.client.node import NodeData
from neon_data_models.models.user.neon_profile import UserProfile
Expand Down Expand Up @@ -79,31 +81,26 @@ def _validate_api_proxy_response(response: dict, query_params: dict):
raise APIError(status_code=code, detail=response['content'])

@staticmethod
def _query_users_api(operation: str, username: str,
password: Optional[str] = None,
access_token: Optional[str] = None,
user: Optional[User] = None) -> (bool, int, Union[User, str]):
def _query_users_api(user_db_request: Union[CreateUserRequest,
ReadUserRequest,
UpdateUserRequest,
DeleteUserRequest]) -> \
(int, Union[User, str]):
"""
Query the users API and return a status code and either a valid User or
a string error message. Authentication may use EITHER a password or
a token.
@param operation: Operation to perform (create, read, update, delete)
@param username: Optional username to include
@param password: Optional password to include
@param access_token: Optional auth token to include
@param user: Optional user object to include
@return: success bool, HTTP status code User object or string error message
@param user_db_request: UserDbRequest object describing CRUD operation
to return
@return: success bool, HTTP status code, User object or string error
"""
response = send_mq_request("/neon_users",
{"operation": operation,
"username": username,
"password": password,
"access_token": access_token,
"user": user.model_dump() if user else None},
"neon_users_input")
user_db_request.model_dump(exclude={
"message_id"}),
target_queue="neon_users_input")
if response.get("success"):
return True, 200, User(**response.get("user"))
return False, response.get("code", 500), response.get("error", "")
return 200, User(**response.get("user"))
return response.get("code", 500), response.get("error", "")

def get_session(self, node_data: NodeData) -> dict:
"""
Expand All @@ -117,65 +114,56 @@ def get_session(self, node_data: NodeData) -> dict:
"site_id": node_data.location.site_id})
return self.sessions_by_id[session_id]

def get_user_profile(self, username: str, password: Optional[str] = None,
access_token: Optional[str] = None) -> User:
def create_user(self, user: User) -> User:
"""
Create a new user.
@param user: User object to add to the users service database
@returns: User object added to the database
"""
create_user_request = CreateUserRequest(user=user, message_id="")
code, err_or_user = self._query_users_api(create_user_request)
if code != 200:
raise HTTPException(status_code=code, detail=err_or_user)
return err_or_user

def read_user(self, username: str, password: Optional[str] = None,
access_token: Optional[str] = None,
auth_user: Optional[str] = None) -> User:
"""
Get a User object for a user. This requires that a valid password OR
access token be provided to prevent arbitrary users from reading
private profile info.
@param username: Valid username to get a User object for
@param password: Valid password to use for authentication
@param access_token: Valid access token to use for authentication
@param auth_user: Optional username to use for authentication
@returns: User object from the Users service.
"""
stat, code, err_or_user = self._query_users_api("read",
username=username,
password=password,
access_token=access_token)
if not stat:
read_user_request = ReadUserRequest(user_spec=username,
auth_user_spec=auth_user,
access_token=access_token,
password=password, message_id="")
code, err_or_user = self._query_users_api(read_user_request)
if code != 200:
raise HTTPException(status_code=code, detail=err_or_user)
return err_or_user

def create_user(self, user: User) -> User:
"""
Create a new user.
@param user: User object to add to the users service database
@returns: User object added to the database
"""
stat, code, err_or_user = self._query_users_api("create",
username=user.username,
password=user.password_hash,
user=user)
if not stat:
raise HTTPException(status_code=code, detail=err_or_user)
return err_or_user

def update_user(self, user: User) -> User:
def update_user(self, user: User,
auth_user: Optional[str] = None,
auth_password: Optional[str] = None) -> User:
"""
Update an existing user in the database.
@param user: Updated user object to write
@param auth_user: Username to use for authentication
@param auth_password: Password associated with `auth_user`
@returns: User as read from the database
"""
stat, code, err_or_user = self._query_users_api("update",
username=user.username,
password=user.password_hash,
user=user)
if not stat:
raise HTTPException(status_code=code, detail=err_or_user)
return err_or_user

def handle_update_user_request(self, user: User, access_token: str):
"""
Handle a request to update a user. This accepts an `auth_token` to
account for requests to change the password or registered tokens.
@param user: Updated User object to write to the database
@param access_token: JWT auth token submitted with the request
"""
stat, code, err_or_user = self._query_users_api("update",
username=user.username,
access_token=access_token,
user=user)
if not stat:
update_user_request = UpdateUserRequest(user=user,
auth_username=auth_user,
auth_password=auth_password,
message_id="")
code, err_or_user = self._query_users_api(update_user_request)
if code != 200:
raise HTTPException(status_code=code, detail=err_or_user)
return err_or_user

Expand Down
9 changes: 8 additions & 1 deletion neon_hana/schema/user_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from pydantic import BaseModel
from typing import Optional

from neon_data_models.models.user.database import User

Expand All @@ -41,9 +42,15 @@ class GetUserRequest(BaseModel):

class UpdateUserRequest(BaseModel):
user: User
auth_username: Optional[str] = None
auth_password: Optional[str] = None

model_config = {
"json_schema_extra": {
"examples": [{
"user": User(username="guest").model_dump()
}]}}
},
{"user": User(username="some_user").model_dump(),
"auth_username": "admin_user",
"auth_password": "admin_password"}
]}}

0 comments on commit 542db86

Please sign in to comment.