Action plugins: typings (WIP)

Fix Bootstrap when token is expired
This commit is contained in:
phil 2024-03-30 17:56:11 +05:30
parent 393096d0b7
commit 3ca56f22a6
4 changed files with 113 additions and 42 deletions

View file

@ -16,7 +16,8 @@ from gisaf.models.authentication import (
) )
from gisaf.models.category import Category, CategoryRead from gisaf.models.category import Category, CategoryRead
from gisaf.models.geo_models_base import GeoModel, PlottableModel 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, DataProvider, ModelValue, PlotParams,
TagActions) TagActions)
from gisaf.models.measures import MeasuresItem from gisaf.models.measures import MeasuresItem
@ -51,7 +52,8 @@ api = APIRouter(
@api.get('/bootstrap') @api.get('/bootstrap')
async def 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) return BootstrapData(user=user)
@ -100,7 +102,7 @@ async def get_roles(
async def get_acls(db_session: db_session, async def get_acls(db_session: db_session,
user: Annotated[User, Depends(get_current_active_user)]) -> list[UserRoleLink]: user: Annotated[User, Depends(get_current_active_user)]) -> list[UserRoleLink]:
"""New: ACLs returned as 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) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
data = await db_session.exec(select(UserRoleLink)) data = await db_session.exec(select(UserRoleLink))
return data.all() # type: ignore[return-value] return data.all() # type: ignore[return-value]
@ -355,6 +357,7 @@ async def get_model_info(
name=name, name=name,
icon=action.icon, icon=action.icon,
formFields=action.formFields, formFields=action.formFields,
roles=action.roles,
) )
for name, actions in plugin_manager.actions_stores.get(store, {}).items() for name, actions in plugin_manager.actions_stores.get(store, {}).items()
for action in actions for action in actions
@ -380,4 +383,36 @@ async def get_plot_params(
# *, db_session: AsyncSession = Depends(get_db_session) # *, db_session: AsyncSession = Depends(get_db_session)
# ) -> list[UserRoleLink]: # ) -> list[UserRoleLink]:
# roles = await db_session.exec(select(UserRoleLink)) # roles = await db_session.exec(select(UserRoleLink))
# return roles.all() # 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

View file

@ -3,15 +3,16 @@ from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
from gisaf.models.info_item import Tag, InfoItem from gisaf.models.info_item import Tag, InfoItem
from gisaf.models.tags import Tags
class ActionResult(BaseModel): # class ActionResult(BaseModel):
message: str # message: str
class ActionResults(BaseModel): # class ActionResults(BaseModel):
name: str # name: str
message: str # message: str
actionResults: list[ActionResult] # actionResults: list[ActionResult]
class DataProvider(BaseModel): class DataProvider(BaseModel):
@ -82,6 +83,7 @@ class FormField(BaseModel):
class ModelAction(BaseModel): class ModelAction(BaseModel):
name: str name: str
icon: str icon: str
roles: list[str] | None = None
formFields: list[FormField] formFields: list[FormField]
@ -160,3 +162,36 @@ class Action(BaseModel):
class ActionsStore(BaseModel): class ActionsStore(BaseModel):
store: str store: str
actions: list[Action] 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] = []

View file

@ -8,7 +8,7 @@ from datetime import datetime
# from aiohttp.web_exceptions import HTTPUnauthorized # from aiohttp.web_exceptions import HTTPUnauthorized
# from aiohttp_security import check_permission # from aiohttp_security import check_permission
from pydantic import BaseModel # noqa: F401 from fastapi import HTTPException, status
from sqlalchemy import or_, and_ from sqlalchemy import or_, and_
# from geoalchemy2.shape import to_shape, from_shape # from geoalchemy2.shape import to_shape, from_shape
# from graphene import ObjectType, String, List, Boolean, Field, Float, InputObjectType # 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.config import conf
from gisaf.models.store import Store # noqa: F401 from gisaf.models.store import Store # noqa: F401
from gisaf.models.tags import Tags as TagsModel from gisaf.models.tags import Tags as TagsModel
from gisaf.models.authentication import UserRead
from gisaf.utils import upsert_df from gisaf.utils import upsert_df
from gisaf.models.reconcile import StatusChange from gisaf.models.reconcile import StatusChange
from gisaf.models.info import ( from gisaf.models.info import (
ActionResults, ActionResults,
ActionResult,
ActionsResults,
Downloader, Downloader,
TagAction, ActionAction, TagAction, ActionAction,
TagActions, TagActions,
TagsStore, TagsStore,
TagsStores, TagsStores,
ActionsStore, ActionsStore,
Action Action,
ActionParam, FormFieldInput,
TaggedLayer, TaggedFeature
) )
from gisaf.registry import NotInRegistry, registry from gisaf.registry import NotInRegistry, registry
@ -57,6 +62,13 @@ class ActionPlugin:
self.icon = icon self.icon = icon
self.form_fields = form_fields or [] 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: class TagPlugin:
""" """
@ -359,7 +371,13 @@ class PluginManager:
#results.append(await action(features_for_action, key, value)) #results.append(await action(features_for_action, key, value))
return ', '.join(results) 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. Execute the plugin action by calling the executor's execute function.
It is up to the plugin action to check for security, using eg: It is up to the plugin action to check for security, using eg:
@ -368,42 +386,22 @@ class PluginManager:
... ...
await check_permission(request, 'role') await check_permission(request, 'role')
""" """
results = [] results = ActionsResults()
try: try:
plugins = self.actions_names[name] plugins = self.actions_names[name]
except KeyError: except KeyError:
raise NoSuchAction raise NoSuchAction
for executor in self.executors[name]: for executor in self.executors[name]:
## TODO: get features from DB? ## TODO: get features from DB?
result: ActionResults
## Check permission ## Check permission
if executor.roles: if executor.roles:
authorized = False if user is None or not any([user.has_role(role) for role in executor.roles]):
for role in executor.roles: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
try: result = await executor.execute(user, features, params, form_fields)
await check_permission(request, role) result.name = name
except HTTPUnauthorized as err: results.actionResults.append(result)
pass return results
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
)
#for store, ids in all_features.items(): #for store, ids in all_features.items():
# actions = self.actions_stores[store] # actions = self.actions_stores[store]

View file

@ -115,7 +115,10 @@ async def get_current_user(
if username == '': if username == '':
raise credentials_exception raise credentials_exception
except ExpiredSignatureError: 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: except JWTError:
raise credentials_exception raise credentials_exception
async with db_session() as session: async with db_session() as session: