296 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Python
		
	
	
| # Copyright 2014-2016 OpenMarket Ltd
 | |
| #
 | |
| # Licensed under the Apache License, Version 2.0 (the "License");
 | |
| # you may not use this file except in compliance with the License.
 | |
| # You may obtain a copy of the License at
 | |
| #
 | |
| #     http://www.apache.org/licenses/LICENSE-2.0
 | |
| #
 | |
| # Unless required by applicable law or agreed to in writing, software
 | |
| # distributed under the License is distributed on an "AS IS" BASIS,
 | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| # See the License for the specific language governing permissions and
 | |
| # limitations under the License.
 | |
| 
 | |
| from typing import TYPE_CHECKING, List, Sequence, Tuple, Union
 | |
| 
 | |
| from synapse.api.errors import (
 | |
|     NotFoundError,
 | |
|     StoreError,
 | |
|     SynapseError,
 | |
|     UnrecognizedRequestError,
 | |
| )
 | |
| from synapse.handlers.push_rules import InvalidRuleException, RuleSpec, check_actions
 | |
| from synapse.http.server import HttpServer
 | |
| from synapse.http.servlet import (
 | |
|     RestServlet,
 | |
|     parse_json_value_from_request,
 | |
|     parse_string,
 | |
| )
 | |
| from synapse.http.site import SynapseRequest
 | |
| from synapse.push.clientformat import format_push_rules_for_user
 | |
| from synapse.push.rulekinds import PRIORITY_CLASS_MAP
 | |
| from synapse.rest.client._base import client_patterns
 | |
| from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException
 | |
| from synapse.types import JsonDict
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from synapse.server import HomeServer
 | |
| 
 | |
| 
 | |
| class PushRuleRestServlet(RestServlet):
 | |
|     PATTERNS = client_patterns("/(?P<path>pushrules/.*)$", v1=True)
 | |
|     SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR = (
 | |
|         "Unrecognised request: You probably wanted a trailing slash"
 | |
|     )
 | |
| 
 | |
|     def __init__(self, hs: "HomeServer"):
 | |
|         super().__init__()
 | |
|         self.auth = hs.get_auth()
 | |
|         self.store = hs.get_datastores().main
 | |
|         self.notifier = hs.get_notifier()
 | |
|         self._is_worker = hs.config.worker.worker_app is not None
 | |
|         self._push_rules_handler = hs.get_push_rules_handler()
 | |
| 
 | |
|     async def on_PUT(self, request: SynapseRequest, path: str) -> Tuple[int, JsonDict]:
 | |
|         if self._is_worker:
 | |
|             raise Exception("Cannot handle PUT /push_rules on worker")
 | |
| 
 | |
|         spec = _rule_spec_from_path(path.split("/"))
 | |
|         try:
 | |
|             priority_class = _priority_class_from_spec(spec)
 | |
|         except InvalidRuleException as e:
 | |
|             raise SynapseError(400, str(e))
 | |
| 
 | |
|         requester = await self.auth.get_user_by_req(request)
 | |
| 
 | |
|         if "/" in spec.rule_id or "\\" in spec.rule_id:
 | |
|             raise SynapseError(400, "rule_id may not contain slashes")
 | |
| 
 | |
|         content = parse_json_value_from_request(request)
 | |
| 
 | |
|         user_id = requester.user.to_string()
 | |
| 
 | |
|         if spec.attr:
 | |
|             try:
 | |
|                 await self._push_rules_handler.set_rule_attr(user_id, spec, content)
 | |
|             except InvalidRuleException as e:
 | |
|                 raise SynapseError(400, "Invalid actions: %s" % e)
 | |
|             except RuleNotFoundException:
 | |
|                 raise NotFoundError("Unknown rule")
 | |
| 
 | |
|             return 200, {}
 | |
| 
 | |
|         if spec.rule_id.startswith("."):
 | |
|             # Rule ids starting with '.' are reserved for server default rules.
 | |
|             raise SynapseError(400, "cannot add new rule_ids that start with '.'")
 | |
| 
 | |
|         try:
 | |
|             (conditions, actions) = _rule_tuple_from_request_object(
 | |
|                 spec.template, spec.rule_id, content
 | |
|             )
 | |
|         except InvalidRuleException as e:
 | |
|             raise SynapseError(400, str(e))
 | |
| 
 | |
|         before = parse_string(request, "before")
 | |
|         if before:
 | |
|             before = f"global/{spec.template}/{before}"
 | |
| 
 | |
|         after = parse_string(request, "after")
 | |
|         if after:
 | |
|             after = f"global/{spec.template}/{after}"
 | |
| 
 | |
|         try:
 | |
|             await self.store.add_push_rule(
 | |
|                 user_id=user_id,
 | |
|                 rule_id=f"global/{spec.template}/{spec.rule_id}",
 | |
|                 priority_class=priority_class,
 | |
|                 conditions=conditions,
 | |
|                 actions=actions,
 | |
|                 before=before,
 | |
|                 after=after,
 | |
|             )
 | |
|             self._push_rules_handler.notify_user(user_id)
 | |
|         except InconsistentRuleException as e:
 | |
|             raise SynapseError(400, str(e))
 | |
|         except RuleNotFoundException as e:
 | |
|             raise SynapseError(400, str(e))
 | |
| 
 | |
|         return 200, {}
 | |
| 
 | |
|     async def on_DELETE(
 | |
|         self, request: SynapseRequest, path: str
 | |
|     ) -> Tuple[int, JsonDict]:
 | |
|         if self._is_worker:
 | |
|             raise Exception("Cannot handle DELETE /push_rules on worker")
 | |
| 
 | |
|         spec = _rule_spec_from_path(path.split("/"))
 | |
| 
 | |
|         requester = await self.auth.get_user_by_req(request)
 | |
|         user_id = requester.user.to_string()
 | |
| 
 | |
|         namespaced_rule_id = f"global/{spec.template}/{spec.rule_id}"
 | |
| 
 | |
|         try:
 | |
|             await self.store.delete_push_rule(user_id, namespaced_rule_id)
 | |
|             self._push_rules_handler.notify_user(user_id)
 | |
|             return 200, {}
 | |
|         except StoreError as e:
 | |
|             if e.code == 404:
 | |
|                 raise NotFoundError()
 | |
|             else:
 | |
|                 raise
 | |
| 
 | |
|     async def on_GET(self, request: SynapseRequest, path: str) -> Tuple[int, JsonDict]:
 | |
|         requester = await self.auth.get_user_by_req(request)
 | |
|         user_id = requester.user.to_string()
 | |
| 
 | |
|         # we build up the full structure and then decide which bits of it
 | |
|         # to send which means doing unnecessary work sometimes but is
 | |
|         # is probably not going to make a whole lot of difference
 | |
|         rules_raw = await self.store.get_push_rules_for_user(user_id)
 | |
| 
 | |
|         rules = format_push_rules_for_user(requester.user, rules_raw)
 | |
| 
 | |
|         path_parts = path.split("/")[1:]
 | |
| 
 | |
|         if path_parts == []:
 | |
|             # we're a reference impl: pedantry is our job.
 | |
|             raise UnrecognizedRequestError(
 | |
|                 PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
 | |
|             )
 | |
| 
 | |
|         if path_parts[0] == "":
 | |
|             return 200, rules
 | |
|         elif path_parts[0] == "global":
 | |
|             result = _filter_ruleset_with_path(rules["global"], path_parts[1:])
 | |
|             return 200, result
 | |
|         else:
 | |
|             raise UnrecognizedRequestError()
 | |
| 
 | |
| 
 | |
| def _rule_spec_from_path(path: Sequence[str]) -> RuleSpec:
 | |
|     """Turn a sequence of path components into a rule spec
 | |
| 
 | |
|     Args:
 | |
|         path: the URL path components.
 | |
| 
 | |
|     Returns:
 | |
|         rule spec, containing scope/template/rule_id entries, and possibly attr.
 | |
| 
 | |
|     Raises:
 | |
|         UnrecognizedRequestError if the path components cannot be parsed.
 | |
|     """
 | |
|     if len(path) < 2:
 | |
|         raise UnrecognizedRequestError()
 | |
|     if path[0] != "pushrules":
 | |
|         raise UnrecognizedRequestError()
 | |
| 
 | |
|     scope = path[1]
 | |
|     path = path[2:]
 | |
|     if scope != "global":
 | |
|         raise UnrecognizedRequestError()
 | |
| 
 | |
|     if len(path) == 0:
 | |
|         raise UnrecognizedRequestError()
 | |
| 
 | |
|     template = path[0]
 | |
|     path = path[1:]
 | |
| 
 | |
|     if len(path) == 0 or len(path[0]) == 0:
 | |
|         raise UnrecognizedRequestError()
 | |
| 
 | |
|     rule_id = path[0]
 | |
| 
 | |
|     path = path[1:]
 | |
| 
 | |
|     attr = None
 | |
|     if len(path) > 0 and len(path[0]) > 0:
 | |
|         attr = path[0]
 | |
| 
 | |
|     return RuleSpec(scope, template, rule_id, attr)
 | |
| 
 | |
| 
 | |
| def _rule_tuple_from_request_object(
 | |
|     rule_template: str, rule_id: str, req_obj: JsonDict
 | |
| ) -> Tuple[List[JsonDict], List[Union[str, JsonDict]]]:
 | |
|     if rule_template in ["override", "underride"]:
 | |
|         if "conditions" not in req_obj:
 | |
|             raise InvalidRuleException("Missing 'conditions'")
 | |
|         conditions = req_obj["conditions"]
 | |
|         for c in conditions:
 | |
|             if "kind" not in c:
 | |
|                 raise InvalidRuleException("Condition without 'kind'")
 | |
|     elif rule_template == "room":
 | |
|         conditions = [{"kind": "event_match", "key": "room_id", "pattern": rule_id}]
 | |
|     elif rule_template == "sender":
 | |
|         conditions = [{"kind": "event_match", "key": "user_id", "pattern": rule_id}]
 | |
|     elif rule_template == "content":
 | |
|         if "pattern" not in req_obj:
 | |
|             raise InvalidRuleException("Content rule missing 'pattern'")
 | |
|         pat = req_obj["pattern"]
 | |
| 
 | |
|         conditions = [{"kind": "event_match", "key": "content.body", "pattern": pat}]
 | |
|     else:
 | |
|         raise InvalidRuleException("Unknown rule template: %s" % (rule_template,))
 | |
| 
 | |
|     if "actions" not in req_obj:
 | |
|         raise InvalidRuleException("No actions found")
 | |
|     actions = req_obj["actions"]
 | |
| 
 | |
|     check_actions(actions)
 | |
| 
 | |
|     return conditions, actions
 | |
| 
 | |
| 
 | |
| def _filter_ruleset_with_path(ruleset: JsonDict, path: List[str]) -> JsonDict:
 | |
|     if path == []:
 | |
|         raise UnrecognizedRequestError(
 | |
|             PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
 | |
|         )
 | |
| 
 | |
|     if path[0] == "":
 | |
|         return ruleset
 | |
|     template_kind = path[0]
 | |
|     if template_kind not in ruleset:
 | |
|         raise UnrecognizedRequestError()
 | |
|     path = path[1:]
 | |
|     if path == []:
 | |
|         raise UnrecognizedRequestError(
 | |
|             PushRuleRestServlet.SLIGHTLY_PEDANTIC_TRAILING_SLASH_ERROR
 | |
|         )
 | |
|     if path[0] == "":
 | |
|         return ruleset[template_kind]
 | |
|     rule_id = path[0]
 | |
| 
 | |
|     the_rule = None
 | |
|     for r in ruleset[template_kind]:
 | |
|         if r["rule_id"] == rule_id:
 | |
|             the_rule = r
 | |
|     if the_rule is None:
 | |
|         raise NotFoundError()
 | |
| 
 | |
|     path = path[1:]
 | |
|     if len(path) == 0:
 | |
|         return the_rule
 | |
| 
 | |
|     attr = path[0]
 | |
|     if attr in the_rule:
 | |
|         # Make sure we return a JSON object as the attribute may be a
 | |
|         # JSON value.
 | |
|         return {attr: the_rule[attr]}
 | |
|     else:
 | |
|         raise UnrecognizedRequestError()
 | |
| 
 | |
| 
 | |
| def _priority_class_from_spec(spec: RuleSpec) -> int:
 | |
|     if spec.template not in PRIORITY_CLASS_MAP.keys():
 | |
|         raise InvalidRuleException("Unknown template: %s" % (spec.template))
 | |
|     pc = PRIORITY_CLASS_MAP[spec.template]
 | |
| 
 | |
|     return pc
 | |
| 
 | |
| 
 | |
| def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
 | |
|     PushRuleRestServlet(hs).register(http_server)
 |