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.
Completed
Pull Request — master (#8)
by
unknown
05:45
created

_parse_request_body()   A

Complexity

Conditions 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 11
rs 9.4285
cc 3
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.rbac.types import PermissionType
37
from st2common.rbac.decorators import request_user_has_webhook_permission
38
39
http_client = six.moves.http_client
40
41
LOG = logging.getLogger(__name__)
42
43
TRACE_TAG_HEADER = 'St2-Trace-Tag'
44
45
46
class WebhooksController(RestController):
47
    def __init__(self, *args, **kwargs):
48
        super(WebhooksController, self).__init__(*args, **kwargs)
49
        self._hooks = {}
50
        self._base_url = '/webhooks/'
51
        self._trigger_types = WEBHOOK_TRIGGER_TYPES.keys()
52
53
        self._trigger_dispatcher = TriggerDispatcher(LOG)
54
        queue_suffix = self.__class__.__name__
55
        self._trigger_watcher = TriggerWatcher(create_handler=self._handle_create_trigger,
56
                                               update_handler=self._handle_update_trigger,
57
                                               delete_handler=self._handle_delete_trigger,
58
                                               trigger_types=self._trigger_types,
59
                                               queue_suffix=queue_suffix,
60
                                               exclusive=True)
61
        self._trigger_watcher.start()
62
        self._register_webhook_trigger_types()
63
64
    @jsexpose()
65
    def get_all(self):
66
        # Return only the hooks known by this controller.
67
        return [trigger for trigger in six.itervalues(self._hooks)]
68
69
    @jsexpose()
70
    def get_one(self, name):
71
        hook = self._hooks.get(name, None)
72
73
        if not hook:
74
            abort(http_client.NOT_FOUND)
75
            return
76
77
        return hook
78
79
    @request_user_has_webhook_permission(permission_type=PermissionType.WEBHOOK_SEND)
80
    @jsexpose(arg_types=[str], status_code=http_client.ACCEPTED)
81
    def post(self, *args, **kwargs):
82
        hook = '/'.join(args)  # TODO: There must be a better way to do this.
83
84
        # Note: For backward compatibility reasons we default to application/json if content
85
        # type is not explicitly provided
86
        content_type = pecan.request.headers.get('Content-Type', 'application/json')
87
        body = pecan.request.body
88
89
        try:
90
            body = self._parse_request_body(content_type=content_type, body=body)
91
        except Exception as e:
92
            self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)
93
            msg = 'Failed to parse request body "%s": %s' % (body, str(e))
94
            return pecan.abort(http_client.BAD_REQUEST, msg)
95
96
        headers = self._get_headers_as_dict(pecan.request.headers)
97
        # If webhook contains a trace-tag use that else create create a unique trace-tag.
98
        trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),
99
                                                   hook=hook)
100
101
        if hook == 'st2' or hook == 'st2/':
102
            return self._handle_st2_webhook(body, trace_context=trace_context)
103
104
        if not self._is_valid_hook(hook):
105
            self._log_request('Invalid hook.', pecan.request)
106
            msg = 'Webhook %s not registered with st2' % hook
107
            return pecan.abort(http_client.NOT_FOUND, msg)
108
109
        trigger = self._get_trigger_for_hook(hook)
110
        payload = {}
111
112
        payload['headers'] = headers
113
        payload['body'] = body
114
        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
115
116
        return body
117
118
    def _parse_request_body(self, content_type, body):
119
        if content_type == 'application/json':
120
            self._log_request('Parsing request body as JSON', request=pecan.request)
121
            body = json.loads(body)
122
        elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:
123
            self._log_request('Parsing request body as form encoded data', request=pecan.request)
124
            body = urlparse.parse_qs(body)
125
        else:
126
            raise ValueError('Unsupported Content-Type: "%s"' % (content_type))
127
128
        return body
129
130
    def _handle_st2_webhook(self, body, trace_context):
131
        trigger = body.get('trigger', None)
132
        payload = body.get('payload', None)
133
        if not trigger:
134
            msg = 'Trigger not specified.'
135
            return pecan.abort(http_client.BAD_REQUEST, msg)
136
        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
137
138
        return body
139
140
    def _is_valid_hook(self, hook):
141
        # TODO: Validate hook payload with payload_schema.
142
        return hook in self._hooks
143
144
    def _get_trigger_for_hook(self, hook):
145
        return self._hooks[hook]
146
147
    def _register_webhook_trigger_types(self):
148
        for trigger_type in WEBHOOK_TRIGGER_TYPES.values():
149
            trigger_service.create_trigger_type_db(trigger_type)
150
151
    def _create_trace_context(self, trace_tag, hook):
152
        # if no trace_tag then create a unique one
153
        if not trace_tag:
154
            trace_tag = 'webhook-%s-%s' % (hook, uuid.uuid4().hex)
155
        return TraceContext(trace_tag=trace_tag)
156
157
    def add_trigger(self, trigger):
158
        # Note: Permission checking for creating and deleting a webhook is done during rule
159
        # creation
160
        url = trigger['parameters']['url']
161
        LOG.info('Listening to endpoint: %s', urljoin(self._base_url, url))
162
        self._hooks[url] = trigger
163
164
    def update_trigger(self, trigger):
165
        pass
166
167
    def remove_trigger(self, trigger):
168
        # Note: Permission checking for creating and deleting a webhook is done during rule
169
        # creation
170
        url = trigger['parameters']['url']
171
172
        if url in self._hooks:
173
            LOG.info('Stop listening to endpoint: %s', urljoin(self._base_url, url))
174
            del self._hooks[url]
175
176
    def _get_headers_as_dict(self, headers):
177
        headers_dict = {}
178
        for key, value in headers.items():
179
            headers_dict[key] = value
180
        return headers_dict
181
182
    def _log_request(self, msg, request, log_method=LOG.debug):
183
        headers = self._get_headers_as_dict(request.headers)
184
        body = str(request.body)
185
        log_method('%s\n\trequest.header: %s.\n\trequest.body: %s.', msg, headers, body)
186
187
    ##############################################
188
    # Event handler methods for the trigger events
189
    ##############################################
190
191
    def _handle_create_trigger(self, trigger):
192
        LOG.debug('Calling "add_trigger" method (trigger.type=%s)' % (trigger.type))
193
        trigger = self._sanitize_trigger(trigger=trigger)
194
        self.add_trigger(trigger=trigger)
195
196
    def _handle_update_trigger(self, trigger):
197
        LOG.debug('Calling "update_trigger" method (trigger.type=%s)' % (trigger.type))
198
        trigger = self._sanitize_trigger(trigger=trigger)
199
        self.update_trigger(trigger=trigger)
200
201
    def _handle_delete_trigger(self, trigger):
202
        LOG.debug('Calling "remove_trigger" method (trigger.type=%s)' % (trigger.type))
203
        trigger = self._sanitize_trigger(trigger=trigger)
204
        self.remove_trigger(trigger=trigger)
205
206
    def _sanitize_trigger(self, trigger):
207
        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...
208
        if 'id' in sanitized:
209
            # Friendly objectid rather than the MongoEngine representation.
210
            sanitized['id'] = str(sanitized['id'])
211
        return sanitized
212