Passed
Push — develop ( 58d288...3f57cf )
by Plexxi
07:33 queued 04:35
created

RuleFilter.filter()   D

Complexity

Conditions 9

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
dl 0
loc 50
rs 4
c 0
b 0
f 0
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import six
17
import json
18
import re
19
from jsonpath_rw import parse
20
21
from st2common import log as logging
22
import st2common.operators as criteria_operators
23
from st2common.constants.rules import TRIGGER_PAYLOAD_PREFIX, RULE_TYPE_BACKSTOP, MATCH_CRITERIA
24
from st2common.constants.keyvalue import SYSTEM_SCOPES
25
from st2common.services.keyvalues import KeyValueLookup
26
from st2common.util.templating import render_template_with_system_context
27
28
29
LOG = logging.getLogger('st2reactor.ruleenforcement.filter')
30
31
32
class RuleFilter(object):
33
    def __init__(self, trigger_instance, trigger, rule, extra_info=False):
34
        """
35
        :param trigger_instance: TriggerInstance DB object.
36
        :type trigger_instance: :class:`TriggerInstanceDB``
37
38
        :param trigger: Trigger DB object.
39
        :type trigger: :class:`TriggerDB`
40
41
        :param rule: Rule DB object.
42
        :type rule: :class:`RuleDB`
43
        """
44
        self.trigger_instance = trigger_instance
45
        self.trigger = trigger
46
        self.rule = rule
47
        self.extra_info = extra_info
48
49
        # Base context used with a logger
50
        self._base_logger_context = {
51
            'rule': self.rule,
52
            'trigger': self.trigger,
53
            'trigger_instance': self.trigger_instance
54
        }
55
56
    def filter(self):
57
        """
58
        Return true if the rule is applicable to the provided trigger instance.
59
60
        :rtype: ``bool``
61
        """
62
        LOG.info('Validating rule %s for %s.', self.rule.ref, self.trigger['name'],
63
                 extra=self._base_logger_context)
64
65
        if not self.rule.enabled:
66
            if self.extra_info:
67
                LOG.info('Validation failed for rule %s as it is disabled.', self.rule.ref)
68
            return False
69
70
        criteria = self.rule.criteria
71
        is_rule_applicable = True
72
73
        if criteria and not self.trigger_instance.payload:
74
            return False
75
76
        payload_lookup = PayloadLookup(self.trigger_instance.payload)
77
78
        LOG.debug('Trigger payload: %s', self.trigger_instance.payload,
79
                  extra=self._base_logger_context)
80
81
        for criterion_k in criteria.keys():
82
            criterion_v = criteria[criterion_k]
83
            is_rule_applicable, payload_value, criterion_pattern = self._check_criterion(
84
                criterion_k,
85
                criterion_v,
86
                payload_lookup
87
            )
88
            if not is_rule_applicable:
89
                if self.extra_info:
90
                    criteria_extra_info = '\n'.join([
91
                        '  key: %s' % criterion_k,
92
                        '  pattern: %s' % criterion_pattern,
93
                        '  type: %s' % criterion_v['type'],
94
                        '  payload: %s' % payload_value
95
                    ])
96
                    LOG.info('Validation for rule %s failed on criteria -\n%s', self.rule.ref,
97
                             criteria_extra_info,
98
                             extra=self._base_logger_context)
99
                break
100
101
        if not is_rule_applicable:
102
            LOG.debug('Rule %s not applicable for %s.', self.rule.id, self.trigger['name'],
103
                      extra=self._base_logger_context)
104
105
        return is_rule_applicable
106
107
    def _check_criterion(self, criterion_k, criterion_v, payload_lookup):
108
        if 'type' not in criterion_v:
109
            # Comparison operator type not specified, can't perform a comparison
110
            return (False, None, None)
111
112
        criteria_operator = criterion_v['type']
113
        criteria_pattern = criterion_v.get('pattern', None)
114
115
        # Render the pattern (it can contain a jinja expressions)
116
        try:
117
            criteria_pattern = self._render_criteria_pattern(
118
                criteria_pattern=criteria_pattern,
119
                criteria_context=payload_lookup.context
120
            )
121
        except Exception:
122
            LOG.exception('Failed to render pattern value "%s" for key "%s"' %
123
                          (criteria_pattern, criterion_k), extra=self._base_logger_context)
124
            return (False, None, None)
125
126
        try:
127
            matches = payload_lookup.get_value(criterion_k)
128
            # pick value if only 1 matches else will end up being an array match.
129
            if matches:
130
                payload_value = matches[0] if len(matches) > 0 else matches
131
            else:
132
                payload_value = None
133
        except:
134
            LOG.exception('Failed transforming criteria key %s', criterion_k,
135
                          extra=self._base_logger_context)
136
            return (False, None, None)
137
138
        op_func = criteria_operators.get_operator(criteria_operator)
139
140
        try:
141
            result = op_func(value=payload_value, criteria_pattern=criteria_pattern)
142
        except:
143
            LOG.exception('There might be a problem with the criteria in rule %s.', self.rule,
144
                          extra=self._base_logger_context)
145
            return (False, None, None)
146
147
        return result, payload_value, criteria_pattern
148
149
    def _render_criteria_pattern(self, criteria_pattern, criteria_context):
150
        # Note: Here we want to use strict comparison to None to make sure that
151
        # other falsy values such as integer 0 are handled correctly.
152
        if criteria_pattern is None:
153
            return None
154
155
        if not isinstance(criteria_pattern, six.string_types):
156
            # We only perform rendering if value is a string - rendering a non-string value
157
            # makes no sense
158
            return criteria_pattern
159
160
        LOG.debug(
161
            'Rendering criteria pattern (%s) with context: %s',
162
            criteria_pattern,
163
            criteria_context
164
        )
165
166
        to_complex = False
167
168
        # Check if jinja variable is in criteria_pattern and if so lets ensure
169
        # the proper type is applied to it using to_complex jinja filter
170
        if len(re.findall(MATCH_CRITERIA, criteria_pattern)) > 0:
171
            LOG.debug("Rendering Complex")
172
            complex_criteria_pattern = re.sub(
173
                MATCH_CRITERIA, r'\1\2 | to_complex\3',
174
                criteria_pattern
175
            )
176
177
            try:
178
                criteria_rendered = render_template_with_system_context(
179
                    value=complex_criteria_pattern,
180
                    context=criteria_context
181
                )
182
                criteria_rendered = json.loads(criteria_rendered)
183
                to_complex = True
184
            except ValueError, error:
185
                LOG.debug('Criteria pattern not valid JSON: %s', error)
186
187
        if not to_complex:
188
            criteria_rendered = render_template_with_system_context(
189
                value=criteria_pattern,
190
                context=criteria_context
191
            )
192
193
        LOG.debug(
194
            'Rendered criteria pattern: %s',
195
            criteria_rendered
196
        )
197
198
        return criteria_rendered
199
200
201
class SecondPassRuleFilter(RuleFilter):
202
    """
203
    Special filter that handles all second pass rules. For not these are only
204
    backstop rules i.e. those that can match when no other rule has matched.
205
    """
206
    def __init__(self, trigger_instance, trigger, rule, first_pass_matched):
207
        """
208
        :param trigger_instance: TriggerInstance DB object.
209
        :type trigger_instance: :class:`TriggerInstanceDB``
210
211
        :param trigger: Trigger DB object.
212
        :type trigger: :class:`TriggerDB`
213
214
        :param rule: Rule DB object.
215
        :type rule: :class:`RuleDB`
216
217
        :param first_pass_matched: Rules that matched in the first pass.
218
        :type first_pass_matched: `list`
219
        """
220
        super(SecondPassRuleFilter, self).__init__(trigger_instance, trigger, rule)
221
        self.first_pass_matched = first_pass_matched
222
223
    def filter(self):
224
        # backstop rules only apply if no rule matched in the first pass.
225
        if self.first_pass_matched and self._is_backstop_rule():
226
            return False
227
        return super(SecondPassRuleFilter, self).filter()
228
229
    def _is_backstop_rule(self):
230
        return self.rule.type['ref'] == RULE_TYPE_BACKSTOP
231
232
233
class PayloadLookup(object):
234
235
    def __init__(self, payload):
236
        self.context = {
237
            TRIGGER_PAYLOAD_PREFIX: payload
238
        }
239
240
        for system_scope in SYSTEM_SCOPES:
241
            self.context[system_scope] = KeyValueLookup(scope=system_scope)
242
243
    def get_value(self, lookup_key):
244
        expr = parse(lookup_key)
245
        matches = [match.value for match in expr.find(self.context)]
246
        if not matches:
247
            return None
248
        return matches
249