Action plugins: typings (WIP)
Fix Bootstrap when token is expired
This commit is contained in:
parent
393096d0b7
commit
3ca56f22a6
4 changed files with 113 additions and 42 deletions
|
@ -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
|
||||||
|
@ -381,3 +384,35 @@ async def get_plot_params(
|
||||||
# ) -> 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
|
|
@ -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] = []
|
|
@ -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)
|
|
||||||
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
|
result.name = name
|
||||||
results.append(result)
|
results.actionResults.append(result)
|
||||||
else:
|
return results
|
||||||
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]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue