diff --git a/neon_hana/app/routers/user.py b/neon_hana/app/routers/user.py index 07c638d..5810c8a 100644 --- a/neon_hana/app/routers/user.py +++ b/neon_hana/app/routers/user.py @@ -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)) diff --git a/neon_hana/auth/client_manager.py b/neon_hana/auth/client_manager.py index 90d2921..9bd30d8 100644 --- a/neon_hana/auth/client_manager.py +++ b/neon_hana/auth/client_manager.py @@ -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, @@ -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 @@ -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): diff --git a/neon_hana/mq_service_api.py b/neon_hana/mq_service_api.py index 7bd8f04..4521489 100644 --- a/neon_hana/mq_service_api.py +++ b/neon_hana/mq_service_api.py @@ -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 @@ -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: """ @@ -117,8 +114,21 @@ 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 @@ -126,56 +136,34 @@ def get_user_profile(self, username: str, password: Optional[str] = None, @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 diff --git a/neon_hana/schema/user_requests.py b/neon_hana/schema/user_requests.py index 3311608..3e1618b 100644 --- a/neon_hana/schema/user_requests.py +++ b/neon_hana/schema/user_requests.py @@ -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 @@ -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"} + ]}}