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: