diff --git a/API/auth/__init__.py b/API/auth/__init__.py index 0839309d..85008024 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -5,7 +5,8 @@ from osm_login_python.core import Auth from pydantic import BaseModel, Field -from src.config import ADMIN_IDS, get_oauth_credentials +from src.app import Users +from src.config import get_oauth_credentials class UserRole(Enum): @@ -23,24 +24,26 @@ class AuthUser(BaseModel): osm_auth = Auth(*get_oauth_credentials()) +auth = Users() -def is_admin(osm_id: int): - admin_ids = [int(admin_id) for admin_id in ADMIN_IDS] - return osm_id in admin_ids + +def get_user_from_db(osm_id: int): + user = auth.read_user(osm_id) + return user def login_required(access_token: str = Header(...)): user = AuthUser(**osm_auth.deserialize_access_token(access_token)) - if is_admin(user.id): - user.role = UserRole.ADMIN + db_user = get_user_from_db(user.id) + user.role = db_user["role"] return user def get_optional_user(access_token: str = Header(default=None)) -> AuthUser: if access_token: user = AuthUser(**osm_auth.deserialize_access_token(access_token)) - if is_admin(user.id): - user.role = UserRole.ADMIN + db_user = get_user_from_db(user.id) + user.role = db_user["role"] return user else: # If no token provided, return a user with limited options or guest user @@ -48,6 +51,20 @@ def get_optional_user(access_token: str = Header(default=None)) -> AuthUser: def admin_required(user: AuthUser = Depends(login_required)): - if not is_admin(user.id): + db_user = get_user_from_db(user.id) + print(db_user) + if not db_user["role"] is UserRole.ADMIN.value: raise HTTPException(status_code=403, detail="User is not an admin") return user + + +def staff_required(user: AuthUser = Depends(login_required)): + db_user = get_user_from_db(user.id) + + # admin is staff too + if not ( + db_user["role"] is UserRole.STAFF.value + or db_user["role"] is UserRole.ADMIN.value + ): + raise HTTPException(status_code=403, detail="User is not a staff") + return user diff --git a/API/auth/routers.py b/API/auth/routers.py index 5d428b7c..c572f420 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -1,10 +1,13 @@ import json from fastapi import APIRouter, Depends, Request +from pydantic import BaseModel -from . import AuthUser, admin_required, login_required, osm_auth +from src.app import Users -router = APIRouter(prefix="/auth") +from . import AuthUser, admin_required, login_required, osm_auth, staff_required + +router = APIRouter(prefix="/auth", tags=["Auth"]) @router.get("/login/") @@ -49,3 +52,104 @@ def my_data(user_data: AuthUser = Depends(login_required)): Returns: user_data """ return user_data + + +class User(BaseModel): + osm_id: int + role: int + + +auth = Users() + + +# Create user +@router.post("/users/", response_model=dict) +async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): + """ + Creates a new user and returns the user's information. + + Args: + - params (User): The user data including osm_id and role. + + Returns: + - Dict[str, Any]: A dictionary containing the osm_id of the newly created user. + + Raises: + - HTTPException: If the user creation fails. + """ + return auth.create_user(params.osm_id, params.role) + + +# Read user by osm_id +@router.get("/users/{osm_id}", response_model=dict) +async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): + """ + Retrieves user information based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to retrieve. + + Returns: + - Dict[str, Any]: A dictionary containing user information. + + Raises: + - HTTPException: If the user with the given osm_id is not found. + """ + return auth.read_user(osm_id) + + +# Update user by osm_id +@router.put("/users/{osm_id}", response_model=dict) +async def update_user( + osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) +): + """ + Updates user information based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to update. + - update_data (User): The data to update for the user. + + Returns: + - Dict[str, Any]: A dictionary containing the updated user information. + + Raises: + - HTTPException: If the user with the given osm_id is not found. + """ + return auth.update_user(osm_id, update_data) + + +# Delete user by osm_id +@router.delete("/users/{osm_id}", response_model=dict) +async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): + """ + Deletes a user based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to delete. + + Returns: + - Dict[str, Any]: A dictionary containing the deleted user information. + + Raises: + - HTTPException: If the user with the given osm_id is not found. + """ + return auth.delete_user(osm_id) + + +# Get all users +@router.get("/users/", response_model=list) +async def read_users( + skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) +): + """ + Retrieves a list of users with optional pagination. + + Args: + - skip (int): The number of users to skip (for pagination). + - limit (int): The maximum number of users to retrieve (for pagination). + + Returns: + - List[Dict[str, Any]]: A list of dictionaries containing user information. + """ + return auth.read_users(skip, limit) diff --git a/API/raw_data.py b/API/raw_data.py index ae4b484b..5594b7aa 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -46,7 +46,7 @@ from .api_worker import process_raw_data from .auth import AuthUser, UserRole, get_optional_user -router = APIRouter(prefix="") +router = APIRouter(prefix="", tags=["Extract"]) @router.get("/status/", response_model=StatusResponse) @@ -57,43 +57,6 @@ def check_database_last_updated(): return {"last_updated": result} -def remove_file(path: str) -> None: - """Used for removing temp file dir and its all content after zip file is delivered to user""" - try: - shutil.rmtree(path) - except OSError as ex: - logging.error("Error: %s - %s.", ex.filename, ex.strerror) - - -def watch_s3_upload(url: str, path: str) -> None: - """Watches upload of s3 either it is completed or not and removes the temp file after completion - - Args: - url (_type_): url generated by the script where data will be available - path (_type_): path where temp file is located at - """ - start_time = time.time() - remove_temp_file = True - check_call = requests.head(url).status_code - if check_call != 200: - logging.debug("Upload is not done yet waiting ...") - while check_call != 200: # check until status is not green - check_call = requests.head(url).status_code - if time.time() - start_time > 300: - logging.error( - "Upload time took more than 5 min , Killing watch : %s , URL : %s", - path, - url, - ) - remove_temp_file = False # don't remove the file if upload fails - break - time.sleep(3) # check each 3 second - # once it is verfied file is uploaded finally remove the file - if remove_temp_file: - logging.debug("File is uploaded at %s , flushing out from %s", url, path) - os.unlink(path) - - @router.post("/snapshot/", response_model=SnapshotResponse) @limiter.limit(f"{export_rate_limit}/minute") @version(1) @@ -486,18 +449,17 @@ def get_osm_current_snapshot_as_plain_geojson( Returns: Featurecollection: Geojson """ - if not (user.role == UserRole.STAFF or user.role == UserRole.ADMIN): - area_m2 = area(json.loads(params.geometry.json())) - area_km2 = area_m2 * 1e-6 - if area_km2 > 30: - raise HTTPException( - status_code=400, - detail=[ - { - "msg": f"""Polygon Area {int(area_km2)} Sq.KM is higher than Threshold : 30 Sq.KM""" - } - ], - ) + area_m2 = area(json.loads(params.geometry.json())) + area_km2 = area_m2 * 1e-6 + if area_km2 > 10: + raise HTTPException( + status_code=400, + detail=[ + { + "msg": f"""Polygon Area {int(area_km2)} Sq.KM is higher than Threshold : 10 Sq.KM""" + } + ], + ) params.output_type = "geojson" # always geojson result = RawData(params).extract_plain_geojson() return result diff --git a/API/tasks.py b/API/tasks.py index 1554a685..3c584979 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -6,9 +6,9 @@ from src.validation.models import SnapshotTaskResponse from .api_worker import celery -from .auth import AuthUser, admin_required, login_required +from .auth import AuthUser, admin_required, login_required, staff_required -router = APIRouter(prefix="/tasks") +router = APIRouter(prefix="/tasks", tags=["Tasks"]) @router.get("/status/{task_id}/", response_model=SnapshotTaskResponse) @@ -40,7 +40,7 @@ def get_task_status(task_id): @router.get("/revoke/{task_id}/") @version(1) -def revoke_task(task_id, user: AuthUser = Depends(login_required)): +def revoke_task(task_id, user: AuthUser = Depends(staff_required)): """Revokes task , Terminates if it is executing Args: diff --git a/backend/raw_backend b/backend/raw_backend index 958c532c..abadb345 100644 --- a/backend/raw_backend +++ b/backend/raw_backend @@ -312,7 +312,15 @@ if __name__ == "__main__": if len(update_cmd_list) > 1: run_subprocess_cmd_parallel(update_cmd_list) - + if args.insert: + users_table = [ + "psql", + "-a", + "-f", + os.path.join(working_dir, "sql/users.sql"), + ] + run_subprocess_cmd(users_table) + print("Users table created") if args.insert or args.post_index: ## build post indexes basic_index_cmd = [ diff --git a/backend/sql/users.sql b/backend/sql/users.sql new file mode 100644 index 00000000..56cd6cba --- /dev/null +++ b/backend/sql/users.sql @@ -0,0 +1,7 @@ +DROP TABLE if exists public.users; +CREATE TABLE public.users ( + osm_id int8 NOT NULL, + role int4 NULL DEFAULT 3, + CONSTRAINT users_un UNIQUE (osm_id), + CONSTRAINT valid_role CHECK (role IN (1, 2, 3)) +); diff --git a/docs/src/installation/configurations.md b/docs/src/installation/configurations.md index a118c42f..122a2129 100644 --- a/docs/src/installation/configurations.md +++ b/docs/src/installation/configurations.md @@ -8,6 +8,19 @@ The default configuration file is an ini-style text file named `config.txt` in the project root. +## Users Table + +Users table is present on ```backend/sql/users.sql``` Make sure you have it before moving forward + +``` +psql -a -f backend/sql/users.sql +``` +& Add your admin's OSM ID as admin + +``` +INSERT INTO users (osm_id, role) VALUES (1234, 1); +``` + ## Sections The following sections are recognised. @@ -34,7 +47,6 @@ The following are the different configuration options that are accepted. | `LOGIN_REDIRECT_URI` | `LOGIN_REDIRECT_URI` | `[OAUTH]` | _none_ | Redirect URL set in the OAuth2 application | REQUIRED | | `APP_SECRET_KEY` | `APP_SECRET_KEY` | `[OAUTH]` | _none_ | High-entropy string generated for the application | REQUIRED | | `OSM_URL` | `OSM_URL` | `[OAUTH]` | `https://www.openstreetmap.org` | OSM instance Base URL | OPTIONAL | -| `ADMIN_IDS` | `ADMIN_IDS` | `[OAUTH]` | `00000` | List of Admin OSMId separated by , | OPTIONAL | | `LOG_LEVEL` | `LOG_LEVEL` | `[API_CONFIG]` | `debug` | Application log level; info,debug,warning,error | OPTIONAL | | `RATE_LIMITER_STORAGE_URI` | `RATE_LIMITER_STORAGE_URI` | `[API_CONFIG]` | `redis://redis:6379` | Redis connection string for rate-limiter data | OPTIONAL | | `RATE_LIMIT_PER_MIN` | `RATE_LIMIT_PER_MIN` | `[API_CONFIG]` | `5` | Number of requests per minute before being rate limited | OPTIONAL | @@ -68,7 +80,6 @@ The following are the different configuration options that are accepted. | `LOGIN_REDIRECT_URI` | TBD | Yes | No | | `APP_SECRET_KEY` | TBD | Yes | No | | `OSM_URL` | TBD | Yes | No | -| `ADMIN_IDS` | TBD | Yes | No | | `LOG_LEVEL` | `[API_CONFIG]` | Yes | Yes | | `RATE_LIMITER_STORAGE_URI` | `[API_CONFIG]` | Yes | No | | `RATE_LIMIT_PER_MIN` | `[API_CONFIG]` | Yes | No | diff --git a/src/app.py b/src/app.py index adc91e34..98c04556 100644 --- a/src/app.py +++ b/src/app.py @@ -207,6 +207,138 @@ def close_conn(self): raise err +class Users: + """ + Users class provides CRUD operations for interacting with the 'users' table in the database. + + Methods: + - create_user(osm_id: int, role: int) -> Dict[str, Any]: Inserts a new user into the database. + - read_user(osm_id: int) -> Dict[str, Any]: Retrieves user information based on the given osm_id. + - update_user(osm_id: int, update_data: UserUpdate) -> Dict[str, Any]: Updates user information based on the given osm_id. + - delete_user(osm_id: int) -> Dict[str, Any]: Deletes a user based on the given osm_id. + - read_users(skip: int = 0, limit: int = 10) -> List[Dict[str, Any]]: Retrieves a list of users with optional pagination. + + Usage: + users = Users() + """ + + def __init__(self) -> None: + """ + Initializes an instance of the Auth class, connecting to the database. + """ + dbdict = get_db_connection_params() + self.d_b = Database(dbdict) + self.con, self.cur = self.d_b.connect() + + def create_user(self, osm_id, role): + """ + Inserts a new user into the 'users' table and returns the created user's osm_id. + + Args: + - osm_id (int): The OSM ID of the new user. + - role (int): The role of the new user. + + Returns: + - Dict[str, Any]: A dictionary containing the osm_id of the newly created user. + + Raises: + - HTTPException: If the user creation fails. + """ + query = "INSERT INTO users (osm_id, role) VALUES (%s, %s) RETURNING osm_id;" + params = (osm_id, role) + self.cur.execute(self.cur.mogrify(query, params).decode("utf-8")) + new_osm_id = self.cur.fetchall()[0][0] + self.con.commit() + return {"osm_id": new_osm_id} + + def read_user(self, osm_id): + """ + Retrieves user information based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to retrieve. + + Returns: + - Dict[str, Any]: A dictionary containing user information if the user is found. + If the user is not found, returns a default user with 'role' set to 3. + + Raises: + - HTTPException: If there's an issue with the database query. + """ + query = "SELECT * FROM users WHERE osm_id = %s;" + params = (osm_id,) + self.cur.execute(self.cur.mogrify(query, params).decode("utf-8")) + result = self.cur.fetchall() + + if result: + return dict(result[0]) + else: + # Return a default user with 'role' set to 3 if the user is not found + return {"osm_id": osm_id, "role": 3} + + def update_user(self, osm_id, update_data): + """ + Updates user information based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to update. + - update_data (UserUpdate): The data to update for the user. + + Returns: + - Dict[str, Any]: A dictionary containing the updated user information. + + Raises: + - HTTPException: If the user with the given osm_id is not found. + """ + query = "UPDATE users SET osm_id = %s, role = %s WHERE osm_id = %s RETURNING *;" + params = (update_data.osm_id, update_data.role, osm_id) + self.cur.execute(self.cur.mogrify(query, params).decode("utf-8")) + updated_user = self.cur.fetchall() + self.con.commit() + if updated_user: + return dict(updated_user[0]) + raise HTTPException(status_code=404, detail="User not found") + + def delete_user(self, osm_id): + """ + Deletes a user based on the given osm_id. + + Args: + - osm_id (int): The OSM ID of the user to delete. + + Returns: + - Dict[str, Any]: A dictionary containing the deleted user information. + + Raises: + - HTTPException: If the user with the given osm_id is not found. + """ + query = "DELETE FROM users WHERE osm_id = %s RETURNING *;" + params = (osm_id,) + self.cur.execute(self.cur.mogrify(query, params).decode("utf-8")) + deleted_user = self.cur.fetchall() + self.con.commit() + if deleted_user: + return dict(deleted_user[0]) + raise HTTPException(status_code=404, detail="User not found") + + def read_users(self, skip=0, limit=10): + """ + Retrieves a list of users with optional pagination. + + Args: + - skip (int): The number of users to skip (for pagination). + - limit (int): The maximum number of users to retrieve (for pagination). + + Returns: + - List[Dict[str, Any]]: A list of dictionaries containing user information. + """ + query = "SELECT * FROM users OFFSET %s LIMIT %s;" + params = (skip, limit) + self.cur.execute(self.cur.mogrify(query, params).decode("utf-8")) + users_list = self.cur.fetchall() + return [dict(user) for user in users_list] + + class RawData: """Class responsible for the Rawdata Extraction from available sources , Currently Works for Underpass source Current Snapshot diff --git a/src/config.py b/src/config.py index deaea7df..ace9342d 100644 --- a/src/config.py +++ b/src/config.py @@ -76,12 +76,6 @@ "API_CONFIG", "ENABLE_TILES", fallback=None ) -###### - -ADMIN_IDS = os.environ.get("ADMIN_IDS") or config.get( - "OAUTH", "ADMIN_IDS", fallback="00000" -).split(",") - #################### diff --git a/tests/test_API.py b/tests/test_API.py index 05ef44db..c3a0ed97 100644 --- a/tests/test_API.py +++ b/tests/test_API.py @@ -358,18 +358,22 @@ def test_snapshot_and_filter(): assert check_status == "SUCCESS" -# def test_snapshot_plain(): -# response = client.post( -# "/v1/snapshot/plain/", -# json={ -# "select": ["name"], -# "where": [ -# {"key": "admin_level", "value": ["7"]}, -# {"key": "boundary", "value": ["administrative"]}, -# {"key": "name", "value": ["Pokhara"]}, -# ], -# "joinBy": "AND", -# "lookIn": ["relations"], -# }, -# ) -# assert response.status_code == 200 +def test_snapshot_plain(): + response = client.post( + "/v1/snapshot/plain/", + json={ + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [83.96919250488281, 28.194446860487773], + [83.99751663208006, 28.194446860487773], + [83.99751663208006, 28.214869548073377], + [83.96919250488281, 28.214869548073377], + [83.96919250488281, 28.194446860487773], + ] + ], + } + }, + ) + assert response.status_code == 200