GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — kale/submit-debug-info ( c0cb8c...f2d693 )
by
unknown
08:15
created

st2api.controllers.v1.HooksHolder   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 38
Duplicated Lines 0 %
Metric Value
wmc 13
dl 0
loc 38
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 2 1
A add_hook() 0 4 2
A get_all() 0 5 2
A __contains__() 0 2 1
A get_triggers_for_hook() 0 2 1
B remove_hook() 0 14 6
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
try:
17
    import simplejson as json
18
except ImportError:
19
    import json
20
21
import six
22
import pecan
23
import uuid
24
from pecan import abort
25
from pecan.rest import RestController
26
from six.moves.urllib import parse as urlparse
27
urljoin = urlparse.urljoin
28
29
from st2common import log as logging
30
from st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES
31
from st2common.models.api.base import jsexpose
32
from st2common.models.api.trace import TraceContext
33
import st2common.services.triggers as trigger_service
34
from st2common.services.triggerwatcher import TriggerWatcher
35
from st2common.transport.reactor import TriggerDispatcher
36
from st2common.util.http import parse_content_type_header
37
from st2common.rbac.types import PermissionType
38
from st2common.rbac.decorators import request_user_has_webhook_permission
39
40
http_client = six.moves.http_client
41
42
LOG = logging.getLogger(__name__)
43
44
TRACE_TAG_HEADER = 'St2-Trace-Tag'
45
46
47
class HooksHolder(object):
48
    """
49
    Maintains a hook to Trigger mapping.
50
    """
51
    def __init__(self):
52
        self._triggers_by_hook = {}
53
54
    def __contains__(self, key):
55
        return key in self._triggers_by_hook
56
57
    def add_hook(self, hook, trigger):
58
        if hook not in self._triggers_by_hook:
59
            self._triggers_by_hook[hook] = []
60
        self._triggers_by_hook[hook].append(trigger)
61
62
    def remove_hook(self, hook, trigger):
63
        if hook not in self._triggers_by_hook:
64
            return False
65
        remove_index = -1
66
        for idx, item in enumerate(self._triggers_by_hook[hook]):
67
            if item['id'] == trigger['id']:
68
                remove_index = idx
69
                break
70
        if remove_index < 0:
71
            return False
72
        self._triggers_by_hook[hook].pop(remove_index)
73
        if not self._triggers_by_hook[hook]:
74
            del self._triggers_by_hook[hook]
75
        return True
76
77
    def get_triggers_for_hook(self, hook):
78
        return self._triggers_by_hook.get(hook, [])
79
80
    def get_all(self):
81
        triggers = []
82
        for values in six.itervalues(self._triggers_by_hook):
83
            triggers.extend(values)
84
        return triggers
85
86
87
class WebhooksController(RestController):
88
    def __init__(self, *args, **kwargs):
89
        super(WebhooksController, self).__init__(*args, **kwargs)
90
        self._hooks = HooksHolder()
91
        self._base_url = '/webhooks/'
92
        self._trigger_types = WEBHOOK_TRIGGER_TYPES.keys()
93
94
        self._trigger_dispatcher = TriggerDispatcher(LOG)
95
        queue_suffix = self.__class__.__name__
96
        self._trigger_watcher = TriggerWatcher(create_handler=self._handle_create_trigger,
97
                                               update_handler=self._handle_update_trigger,
98
                                               delete_handler=self._handle_delete_trigger,
99
                                               trigger_types=self._trigger_types,
100
                                               queue_suffix=queue_suffix,
101
                                               exclusive=True)
102
        self._trigger_watcher.start()
103
        self._register_webhook_trigger_types()
104
105
    @jsexpose()
106
    def get_all(self):
107
        # Return only the hooks known by this controller.
108
        return self._hooks.get_all()
109
110
    @jsexpose()
111
    def get_one(self, name):
112
        triggers = self._hooks.get_triggers_for_hook(name)
113
114
        if not triggers:
115
            abort(http_client.NOT_FOUND)
116
            return
117
118
        # For demonstration purpose return 1st
119
        return triggers[0]
120
121
    @request_user_has_webhook_permission(permission_type=PermissionType.WEBHOOK_SEND)
122
    @jsexpose(arg_types=[str], status_code=http_client.ACCEPTED)
123
    def post(self, *args, **kwargs):
124
        hook = '/'.join(args)  # TODO: There must be a better way to do this.
125
126
        # Note: For backward compatibility reasons we default to application/json if content
127
        # type is not explicitly provided
128
        content_type = pecan.request.headers.get('Content-Type', 'application/json')
129
        content_type = parse_content_type_header(content_type=content_type)[0]
130
        body = pecan.request.body
131
132
        try:
133
            body = self._parse_request_body(content_type=content_type, body=body)
134
        except Exception as e:
135
            self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)
136
            msg = 'Failed to parse request body "%s": %s' % (body, str(e))
137
            return pecan.abort(http_client.BAD_REQUEST, msg)
138
139
        headers = self._get_headers_as_dict(pecan.request.headers)
140
        # If webhook contains a trace-tag use that else create create a unique trace-tag.
141
        trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),
142
                                                   hook=hook)
143
144
        if hook == 'st2' or hook == 'st2/':
145
            return self._handle_st2_webhook(body, trace_context=trace_context)
146
147
        if not self._is_valid_hook(hook):
148
            self._log_request('Invalid hook.', pecan.request)
149
            msg = 'Webhook %s not registered with st2' % hook
150
            return pecan.abort(http_client.NOT_FOUND, msg)
151
152
        triggers = self._hooks.get_triggers_for_hook(hook)
153
        payload = {}
154
155
        payload['headers'] = headers
156
        payload['body'] = body
157
        # Dispatch trigger instance for each of the trigger found
158
        for trigger in triggers:
159
            self._trigger_dispatcher.dispatch(trigger, payload=payload,
160
                trace_context=trace_context)
161
162
        return body
163
164
    def _parse_request_body(self, content_type, body):
165
        if content_type == 'application/json':
166
            self._log_request('Parsing request body as JSON', request=pecan.request)
167
            body = json.loads(body)
168
        elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:
169
            self._log_request('Parsing request body as form encoded data', request=pecan.request)
170
            body = urlparse.parse_qs(body)
171
        else:
172
            raise ValueError('Unsupported Content-Type: "%s"' % (content_type))
173
174
        return body
175
176
    def _handle_st2_webhook(self, body, trace_context):
177
        trigger = body.get('trigger', None)
178
        payload = body.get('payload', None)
179
        if not trigger:
180
            msg = 'Trigger not specified.'
181
            return pecan.abort(http_client.BAD_REQUEST, msg)
182
        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
183
184
        return body
185
186
    def _is_valid_hook(self, hook):
187
        # TODO: Validate hook payload with payload_schema.
188
        return hook in self._hooks
189
190
    def _register_webhook_trigger_types(self):
191
        for trigger_type in WEBHOOK_TRIGGER_TYPES.values():
192
            trigger_service.create_trigger_type_db(trigger_type)
193
194
    def _create_trace_context(self, trace_tag, hook):
195
        # if no trace_tag then create a unique one
196
        if not trace_tag:
197
            trace_tag = 'webhook-%s-%s' % (hook, uuid.uuid4().hex)
198
        return TraceContext(trace_tag=trace_tag)
199
200
    def add_trigger(self, trigger):
201
        # Note: Permission checking for creating and deleting a webhook is done during rule
202
        # creation
203
        url = self._get_normalized_url(trigger)
204
        LOG.info('Listening to endpoint: %s', urljoin(self._base_url, url))
205
        self._hooks.add_hook(url, trigger)
206
207
    def update_trigger(self, trigger):
208
        pass
209
210
    def remove_trigger(self, trigger):
211
        # Note: Permission checking for creating and deleting a webhook is done during rule
212
        # creation
213
        url = self._get_normalized_url(trigger)
214
215
        removed = self._hooks.remove_hook(url, trigger)
216
        if removed:
217
            LOG.info('Stop listening to endpoint: %s', urljoin(self._base_url, url))
218
219
    def _get_normalized_url(self, trigger):
220
        """
221
        remove the trailing and leading / so that the hook url and those coming
222
        from trigger parameters end up being the same.
223
        """
224
        return trigger['parameters']['url'].strip('/')
225
226
    def _get_headers_as_dict(self, headers):
227
        headers_dict = {}
228
        for key, value in headers.items():
229
            headers_dict[key] = value
230
        return headers_dict
231
232
    def _log_request(self, msg, request, log_method=LOG.debug):
233
        headers = self._get_headers_as_dict(request.headers)
234
        body = str(request.body)
235
        log_method('%s\n\trequest.header: %s.\n\trequest.body: %s.', msg, headers, body)
236
237
    ##############################################
238
    # Event handler methods for the trigger events
239
    ##############################################
240
241
    def _handle_create_trigger(self, trigger):
242
        LOG.debug('Calling "add_trigger" method (trigger.type=%s)' % (trigger.type))
243
        trigger = self._sanitize_trigger(trigger=trigger)
244
        self.add_trigger(trigger=trigger)
245
246
    def _handle_update_trigger(self, trigger):
247
        LOG.debug('Calling "update_trigger" method (trigger.type=%s)' % (trigger.type))
248
        trigger = self._sanitize_trigger(trigger=trigger)
249
        self.update_trigger(trigger=trigger)
250
251
    def _handle_delete_trigger(self, trigger):
252
        LOG.debug('Calling "remove_trigger" method (trigger.type=%s)' % (trigger.type))
253
        trigger = self._sanitize_trigger(trigger=trigger)
254
        self.remove_trigger(trigger=trigger)
255
256
    def _sanitize_trigger(self, trigger):
257
        sanitized = trigger._data
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _data was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
258
        if 'id' in sanitized:
259
            # Friendly objectid rather than the MongoEngine representation.
260
            sanitized['id'] = str(sanitized['id'])
261
        return sanitized
262