Skip to content

Commit f71e3fb

Browse files
committed
add validation to request handler
1 parent 16cc292 commit f71e3fb

File tree

11 files changed

+151
-73
lines changed

11 files changed

+151
-73
lines changed

docs/plugins.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,22 @@ Actions are used as custom behaviors to requests received by sentinela. If senti
2323

2424
Actions must have the following signature:
2525
```python
26-
async def action_name(message_payload: dict[Any, Any]):
26+
from data_models.request_payload import RequestPayload
27+
28+
29+
async def action_name(message_payload: RequestPayload):
2730
```
2831

32+
The `RequestPayload` object contains the action name and the parameters sent by the request. The parameters will vary depending on the action.
33+
2934
An example of the action call made by Sentinela is:
3035
```python
31-
await plugin.my_plugin.actions.action_name({"key": "value"})
36+
from data_models.request_payload import RequestPayload
37+
38+
39+
await plugin.my_plugin.actions.action_name(
40+
RequestPayload(action="my_plugin.action_name", params={"key": "value"})
41+
)
3242
```
3343

3444
## Notifications

src/commands/requests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def alert_acknowledge(alert_id: int) -> None:
4040
type="request",
4141
payload={
4242
"action": "alert_acknowledge",
43-
"target_id": alert_id,
43+
"params": {"target_id": alert_id},
4444
},
4545
)
4646

@@ -51,7 +51,7 @@ async def alert_lock(alert_id: int) -> None:
5151
type="request",
5252
payload={
5353
"action": "alert_lock",
54-
"target_id": alert_id,
54+
"params": {"target_id": alert_id},
5555
},
5656
)
5757

@@ -62,7 +62,7 @@ async def alert_solve(alert_id: int) -> None:
6262
type="request",
6363
payload={
6464
"action": "alert_solve",
65-
"target_id": alert_id,
65+
"params": {"target_id": alert_id},
6666
},
6767
)
6868

@@ -73,6 +73,6 @@ async def issue_drop(issue_id: int) -> None:
7373
type="request",
7474
payload={
7575
"action": "issue_drop",
76-
"target_id": issue_id,
76+
"params": {"target_id": issue_id},
7777
},
7878
)

src/components/executor/request_handler.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from typing import Any, Callable, Coroutine, cast
66

77
import prometheus_client
8+
from pydantic import ValidationError
89

910
import plugins
1011
import registry as registry
1112
from base_exception import BaseSentinelaException
1213
from configs import configs
14+
from data_models.request_payload import RequestPayload
1315
from models import Alert, Issue
1416

1517
_logger = logging.getLogger("request_handler")
@@ -31,9 +33,9 @@
3133
)
3234

3335

34-
async def alert_acknowledge(message_payload: dict[Any, Any]) -> None:
36+
async def alert_acknowledge(message_payload: RequestPayload) -> None:
3537
"""Acknowledge an alert"""
36-
alert_id = message_payload["target_id"]
38+
alert_id = message_payload.params["target_id"]
3739
alert = await Alert.get_by_id(alert_id)
3840
if alert is None:
3941
_logger.info(f"Alert '{alert_id}' not found")
@@ -42,9 +44,9 @@ async def alert_acknowledge(message_payload: dict[Any, Any]) -> None:
4244
await alert.acknowledge()
4345

4446

45-
async def alert_lock(message_payload: dict[Any, Any]) -> None:
47+
async def alert_lock(message_payload: RequestPayload) -> None:
4648
"""Lock an alert"""
47-
alert_id = message_payload["target_id"]
49+
alert_id = message_payload.params["target_id"]
4850
alert = await Alert.get_by_id(alert_id)
4951
if alert is None:
5052
_logger.info(f"Alert '{alert_id}' not found")
@@ -53,9 +55,9 @@ async def alert_lock(message_payload: dict[Any, Any]) -> None:
5355
await alert.lock()
5456

5557

56-
async def alert_solve(message_payload: dict[Any, Any]) -> None:
58+
async def alert_solve(message_payload: RequestPayload) -> None:
5759
"""Solve all alert's issues"""
58-
alert_id = message_payload["target_id"]
60+
alert_id = message_payload.params["target_id"]
5961
alert = await Alert.get_by_id(alert_id)
6062
if alert is None:
6163
_logger.info(f"Alert '{alert_id}' not found")
@@ -64,9 +66,9 @@ async def alert_solve(message_payload: dict[Any, Any]) -> None:
6466
await alert.solve_issues()
6567

6668

67-
async def issue_drop(message_payload: dict[Any, Any]) -> None:
69+
async def issue_drop(message_payload: RequestPayload) -> None:
6870
"""Drop an issue"""
69-
issue_id = message_payload["target_id"]
71+
issue_id = message_payload.params["target_id"]
7072
issue = await Issue.get_by_id(issue_id)
7173
if issue is None:
7274
_logger.info(f"Issue '{issue_id}' not found")
@@ -83,7 +85,7 @@ async def issue_drop(message_payload: dict[Any, Any]) -> None:
8385
}
8486

8587

86-
def get_action(action_name: str) -> Callable[[dict[Any, Any]], Coroutine[Any, Any, None]] | None:
88+
def get_action(action_name: str) -> Callable[[RequestPayload], Coroutine[Any, Any, None]] | None:
8789
"""Get the action function by its name, checking if it is a plugin action"""
8890
if action_name.startswith("plugin."):
8991
plugin_name, action_name = action_name.split(".")[1:3]
@@ -103,31 +105,41 @@ def get_action(action_name: str) -> Callable[[dict[Any, Any]], Coroutine[Any, An
103105
_logger.warning(f"Action '{plugin_name}.{action_name}' unknown")
104106
return None
105107

106-
return cast(Callable[[dict[Any, Any]], Coroutine[Any, Any, None]], action)
108+
return cast(Callable[[RequestPayload], Coroutine[Any, Any, None]], action)
107109

108110
return actions.get(action_name)
109111

110112

111113
async def run(message: dict[Any, Any]) -> None:
112114
"""Process a received request"""
113-
message_payload = message["payload"]
114-
action_name = message_payload["action"]
115+
try:
116+
message_payload = RequestPayload(**message["payload"])
117+
except KeyError:
118+
_logger.error(f"Message '{json.dumps(message)}' missing 'payload' field")
119+
return
120+
except ValidationError as e:
121+
_logger.error(f"Invalid payload: {e}")
122+
return
123+
124+
action_name = message_payload.action
115125

116126
action = get_action(action_name)
117127

118128
if action is None:
119-
_logger.warning(f"Got request with unknown action '{json.dumps(message_payload)}'")
129+
_logger.warning(
130+
f"Got request with unknown action '{json.dumps(message_payload.to_dict())}'"
131+
)
120132
return
121133

122134
try:
123135
with prometheus_request_execution_time.labels(action_name=action_name).time():
124136
await asyncio.wait_for(action(message_payload), configs.executor_request_timeout)
125137
except asyncio.TimeoutError:
126138
prometheus_request_timeout_count.labels(action_name=action_name).inc()
127-
_logger.error(f"Timed out executing request '{json.dumps(message_payload)}'")
139+
_logger.error(f"Timed out executing request '{json.dumps(message_payload.to_dict())}'")
128140
except BaseSentinelaException as e:
129141
raise e
130142
except Exception:
131143
prometheus_request_error_count.labels(action_name=action_name).inc()
132-
_logger.error(f"Error executing request '{json.dumps(message_payload)}'")
144+
_logger.error(f"Error executing request '{json.dumps(message_payload.to_dict())}'")
133145
_logger.error(traceback.format_exc().strip())
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .request_payload import RequestPayload
2+
3+
__all__ = ["RequestPayload"]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Any
2+
3+
from pydantic.dataclasses import dataclass
4+
5+
6+
@dataclass
7+
class RequestPayload:
8+
action: str
9+
params: dict[str, Any]
10+
11+
def to_dict(self) -> dict[str, Any]:
12+
return {
13+
field: value
14+
for field in self.__dataclass_fields__
15+
if (value := getattr(self, field)) is not None
16+
}

src/plugins/slack/services/pattern_match.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def resend_notifications(
5353
type="request",
5454
payload={
5555
"action": "plugin.slack.resend_notifications",
56-
"slack_channel": context["channel"],
56+
"params": {"slack_channel": context["channel"]},
5757
},
5858
)
5959

tests/commands/test_requests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def test_alert_acknowledge(clear_queue, target_id):
9494
"type": "request",
9595
"payload": {
9696
"action": "alert_acknowledge",
97-
"target_id": target_id,
97+
"params": {"target_id": target_id},
9898
},
9999
}
100100
)
@@ -114,7 +114,7 @@ async def test_alert_lock(clear_queue, target_id):
114114
"type": "request",
115115
"payload": {
116116
"action": "alert_lock",
117-
"target_id": target_id,
117+
"params": {"target_id": target_id},
118118
},
119119
}
120120
)
@@ -134,7 +134,7 @@ async def test_alert_solve(clear_queue, target_id):
134134
"type": "request",
135135
"payload": {
136136
"action": "alert_solve",
137-
"target_id": target_id,
137+
"params": {"target_id": target_id},
138138
},
139139
}
140140
)
@@ -154,7 +154,7 @@ async def test_issue_drop(clear_queue, target_id):
154154
"type": "request",
155155
"payload": {
156156
"action": "issue_drop",
157-
"target_id": target_id,
157+
"params": {"target_id": target_id},
158158
},
159159
}
160160
)

0 commit comments

Comments
 (0)