From 3ca56f22a63e385c0ddf5e021061de320d2c5640 Mon Sep 17 00:00:00 2001 From: phil Date: Sat, 30 Mar 2024 17:56:11 +0530 Subject: [PATCH] Action plugins: typings (WIP) Fix Bootstrap when token is expired --- src/gisaf/api/main.py | 43 +++++++++++++++++++++++++--- src/gisaf/models/info.py | 47 +++++++++++++++++++++++++++---- src/gisaf/plugins.py | 60 +++++++++++++++++++--------------------- src/gisaf/security.py | 5 +++- 4 files changed, 113 insertions(+), 42 deletions(-) diff --git a/src/gisaf/api/main.py b/src/gisaf/api/main.py index 7366ca2..0f79651 100644 --- a/src/gisaf/api/main.py +++ b/src/gisaf/api/main.py @@ -16,7 +16,8 @@ from gisaf.models.authentication import ( ) from gisaf.models.category import Category, CategoryRead from gisaf.models.geo_models_base import GeoModel, PlottableModel -from gisaf.models.info import (LegendItem, ModelAction, ModelInfo, +from gisaf.models.info import (ActionParam, ActionResult, ActionResults, ActionsResults, ActionsStore, FormFieldInput, LegendItem, + ModelAction, ModelInfo, DataProvider, ModelValue, PlotParams, TagActions) from gisaf.models.measures import MeasuresItem @@ -51,7 +52,8 @@ api = APIRouter( @api.get('/bootstrap') async def bootstrap( - user: Annotated[UserRead, Depends(get_current_active_user)]) -> BootstrapData: + user: Annotated[UserRead, Depends(get_current_active_user)] + ) -> BootstrapData: return BootstrapData(user=user) @@ -100,7 +102,7 @@ async def get_roles( async def get_acls(db_session: db_session, user: Annotated[User, Depends(get_current_active_user)]) -> list[UserRoleLink]: """New: ACLs returned as UserRoleLink""" - if not user or not user.has_role('manager'): + if user is not None or not user.has_role('manager'): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) data = await db_session.exec(select(UserRoleLink)) return data.all() # type: ignore[return-value] @@ -355,6 +357,7 @@ async def get_model_info( name=name, icon=action.icon, formFields=action.formFields, + roles=action.roles, ) for name, actions in plugin_manager.actions_stores.get(store, {}).items() for action in actions @@ -380,4 +383,36 @@ async def get_plot_params( # *, db_session: AsyncSession = Depends(get_db_session) # ) -> list[UserRoleLink]: # roles = await db_session.exec(select(UserRoleLink)) -# return roles.all() \ No newline at end of file +# return roles.all() + +@api.get('/actions') +async def get_actions() -> list[ActionsStore]: + # actionsPlugins = List(ActionsStore) + return plugin_manager.actionsStores + +@api.post('/execTagAction/{action}') +async def execute_tag_action( + user: Annotated[UserRead, Depends(get_current_active_user)], + stores: list[str], + ids: list[list[str]], + actionNames: list[str], + params: list[ActionParam | None], + formFields: list[FormFieldInput], + ) -> ActionsResults: + features = dict(zip(stores, [[int(id) for id in _ids] for _ids in ids])) + response = ActionsResults() + #formFields = {field['name']: field['value'] for field in formFields} + if not params: + params = [None] * len(actionNames) + for name in actionNames: + try: + ## Give the request from context to execute action, along with the parameters + ## FIXME: formFields/names? + breakpoint() + resp = await plugin_manager.execute_action( + user, features, name, params, form_fields=formFields) + response.actionResults.append(resp) + except NoSuchAction: + logger.warn(f'Unknown action {name}') + response.actionResults.append(ActionResult(message=f'No such action: {name}')) + return response \ No newline at end of file diff --git a/src/gisaf/models/info.py b/src/gisaf/models/info.py index 5a2bae4..65fc393 100644 --- a/src/gisaf/models/info.py +++ b/src/gisaf/models/info.py @@ -3,15 +3,16 @@ from typing import Any from pydantic import BaseModel from gisaf.models.info_item import Tag, InfoItem +from gisaf.models.tags import Tags -class ActionResult(BaseModel): - message: str +# class ActionResult(BaseModel): +# message: str -class ActionResults(BaseModel): - name: str - message: str - actionResults: list[ActionResult] +# class ActionResults(BaseModel): +# name: str +# message: str +# actionResults: list[ActionResult] class DataProvider(BaseModel): @@ -82,6 +83,7 @@ class FormField(BaseModel): class ModelAction(BaseModel): name: str icon: str + roles: list[str] | None = None formFields: list[FormField] @@ -160,3 +162,36 @@ class Action(BaseModel): class ActionsStore(BaseModel): store: str actions: list[Action] + + +class FormFieldInput(BaseModel): + name: str + value: str + + +class TaggedFeature(BaseModel): + id: str + tags: Tags + lat: float + lon: float + + +class TaggedLayer(BaseModel): + store: str + taggedFeatures: list[TaggedFeature] + + +class ActionResult(BaseModel): + message: str | None = None + taggedLayers: list[TaggedLayer] = [] + + +class ActionResults(BaseModel): + name: str | None = None + message: str | None = None + actionResults: list[ActionResult] = [] + + +class ActionsResults(BaseModel): + message: str | None = None + actionResults: list[ActionResults] = [] \ No newline at end of file diff --git a/src/gisaf/plugins.py b/src/gisaf/plugins.py index e6d4099..07ceae2 100644 --- a/src/gisaf/plugins.py +++ b/src/gisaf/plugins.py @@ -8,7 +8,7 @@ from datetime import datetime # from aiohttp.web_exceptions import HTTPUnauthorized # from aiohttp_security import check_permission -from pydantic import BaseModel # noqa: F401 +from fastapi import HTTPException, status from sqlalchemy import or_, and_ # from geoalchemy2.shape import to_shape, from_shape # from graphene import ObjectType, String, List, Boolean, Field, Float, InputObjectType @@ -19,17 +19,22 @@ import shapely # type: ignore from gisaf.config import conf from gisaf.models.store import Store # noqa: F401 from gisaf.models.tags import Tags as TagsModel +from gisaf.models.authentication import UserRead from gisaf.utils import upsert_df from gisaf.models.reconcile import StatusChange from gisaf.models.info import ( ActionResults, + ActionResult, + ActionsResults, Downloader, TagAction, ActionAction, TagActions, TagsStore, TagsStores, ActionsStore, - Action + Action, + ActionParam, FormFieldInput, + TaggedLayer, TaggedFeature ) from gisaf.registry import NotInRegistry, registry @@ -57,6 +62,13 @@ class ActionPlugin: self.icon = icon self.form_fields = form_fields or [] + async def execute(self, + user: UserRead, + features: dict[str, list], + params: list, + form_fields: list[FormFieldInput]) -> ActionResults: + raise NotImplementedError('Action plugins must implement execute') + class TagPlugin: """ @@ -359,7 +371,13 @@ class PluginManager: #results.append(await action(features_for_action, key, value)) return ', '.join(results) - async def execute_action(self, request, features, name, params, form_fields): + async def execute_action(self, + user: UserRead, + features: dict, + name: str, + params: list[ActionParam | None], + form_fields: list[FormFieldInput] + ) -> ActionsResults: """ Execute the plugin action by calling the executor's execute function. It is up to the plugin action to check for security, using eg: @@ -368,42 +386,22 @@ class PluginManager: ... await check_permission(request, 'role') """ - results = [] + results = ActionsResults() try: plugins = self.actions_names[name] except KeyError: raise NoSuchAction for executor in self.executors[name]: ## TODO: get features from DB? - + result: ActionResults ## Check permission if executor.roles: - authorized = False - for role in executor.roles: - try: - await check_permission(request, role) - except HTTPUnauthorized as err: - pass - else: - authorized = True - break - else: - ## No roles: OK for anonymous - authorized = True - - if authorized: - result = await executor.execute( - request, features, params, - **{field['name']: field for field in form_fields} - ) - result.name = name - results.append(result) - else: - raise HTTPUnauthorized - - return ActionsResults( - actionResults=results - ) + if user is None or not any([user.has_role(role) for role in executor.roles]): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + result = await executor.execute(user, features, params, form_fields) + result.name = name + results.actionResults.append(result) + return results #for store, ids in all_features.items(): # actions = self.actions_stores[store] diff --git a/src/gisaf/security.py b/src/gisaf/security.py index 7d13c01..2e5670b 100644 --- a/src/gisaf/security.py +++ b/src/gisaf/security.py @@ -115,7 +115,10 @@ async def get_current_user( if username == '': raise credentials_exception except ExpiredSignatureError: - raise expired_exception + # raise expired_exception + decoded = jwt.get_unverified_claims(token) + logger.debug(f"Session expired for user {decoded.get('sub')}") + return None except JWTError: raise credentials_exception async with db_session() as session: