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 — plexxi-v2.3.2 ( 5d46fe )
by
unknown
06:59
created

BaseCLIApp._print_client_settings()   B

Complexity

Conditions 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 24
rs 8.9713
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 os
17
import json
18
import logging
19
import time
20
import calendar
21
import traceback
22
23
import six
24
import requests
25
26
from st2client import models
27
from st2client.config_parser import CLIConfigParser
28
from st2client.config_parser import ST2_CONFIG_DIRECTORY
29
from st2client.config_parser import ST2_CONFIG_PATH
30
from st2client.client import Client
31
from st2client.config import get_config
32
from st2client.utils.date import parse as parse_isotime
33
from st2client.utils.misc import merge_dicts
34
35
__all__ = [
36
    'BaseCLIApp'
37
]
38
39
# How many seconds before the token actual expiration date we should consider the token as
40
# expired. This is used to prevent the operation from failing durig the API request because the
41
# token was just about to expire.
42
TOKEN_EXPIRATION_GRACE_PERIOD_SECONDS = 15
43
44
CONFIG_OPTION_TO_CLIENT_KWARGS_MAP = {
45
    'base_url': ['general', 'base_url'],
46
    'auth_url': ['auth', 'url'],
47
    'stream_url': ['stream', 'url'],
48
    'api_url': ['api', 'url'],
49
    'api_version': ['general', 'api_version'],
50
    'api_key': ['credentials', 'api_key'],
51
    'cacert': ['general', 'cacert'],
52
    'debug': ['cli', 'debug']
53
}
54
55
56
class BaseCLIApp(object):
57
    """
58
    Base class for StackStorm CLI apps.
59
    """
60
61
    LOG = logging.getLogger(__name__)  # logger instance to use
62
    client = None  # st2client instance
63
64
    # A list of command classes for which automatic authentication should be skipped.
65
    SKIP_AUTH_CLASSES = []
66
67
    def get_client(self, args, debug=False):
68
        ST2_CLI_SKIP_CONFIG = os.environ.get('ST2_CLI_SKIP_CONFIG', 0)
69
        ST2_CLI_SKIP_CONFIG = int(ST2_CLI_SKIP_CONFIG)
70
71
        skip_config = args.skip_config
72
        skip_config = skip_config or ST2_CLI_SKIP_CONFIG
73
74
        # Note: Options provided as the CLI argument have the highest precedence
75
        # Precedence order: cli arguments > environment variables > rc file variables
76
        cli_options = ['base_url', 'auth_url', 'api_url', 'stream_url', 'api_version', 'cacert']
77
        cli_options = {opt: getattr(args, opt, None) for opt in cli_options}
78
        config_file_options = self._get_config_file_options(args=args)
79
80
        kwargs = {}
81
82
        if not skip_config:
83
            # Config parsing is not skipped
84
            kwargs = merge_dicts(kwargs, config_file_options)
85
86
        kwargs = merge_dicts(kwargs, cli_options)
87
        kwargs['debug'] = debug
88
89
        client = Client(**kwargs)
90
91
        if skip_config:
92
            # Config parsing is skipped
93
            self.LOG.info('Skipping parsing CLI config')
94
            return client
95
96
        # Ok to use config at this point
97
        rc_config = get_config()
98
99
        # Silence SSL warnings
100
        silence_ssl_warnings = rc_config.get('general', {}).get('silence_ssl_warnings', False)
101
        if silence_ssl_warnings:
102
            requests.packages.urllib3.disable_warnings()
103
104
        # We skip automatic authentication for some commands such as auth
105
        try:
106
            command_class_name = args.func.im_class.__name__
107
        except Exception:
108
            command_class_name = None
109
110
        if command_class_name in self.SKIP_AUTH_CLASSES:
111
            return client
112
113
        # We also skip automatic authentication if token is provided via the environment variable
114
        # or as a command line argument
115
        env_var_token = os.environ.get('ST2_AUTH_TOKEN', None)
116
        cli_argument_token = getattr(args, 'token', None)
117
        env_var_api_key = os.environ.get('ST2_API_KEY', None)
118
        cli_argument_api_key = getattr(args, 'api_key', None)
119
        if env_var_token or cli_argument_token or env_var_api_key or cli_argument_api_key:
120
            return client
121
122
        # If credentials are provided in the CLI config use them and try to authenticate
123
        credentials = rc_config.get('credentials', {})
124
        username = credentials.get('username', None)
125
        password = credentials.get('password', None)
126
        cache_token = rc_config.get('cli', {}).get('cache_token', False)
127
128
        if username:
129
            # Credentials are provided, try to authenticate agaist the API
130
            try:
131
                token = self._get_auth_token(client=client, username=username, password=password,
132
                                             cache_token=cache_token)
133
            except requests.exceptions.ConnectionError as e:
134
                self.LOG.warn('Auth API server is not available, skipping authentication.')
135
                self.LOG.exception(e)
136
                return client
137
            except Exception as e:
138
                print('Failed to authenticate with credentials provided in the config.')
139
                raise e
140
            client.token = token
141
            # TODO: Hack, refactor when splitting out the client
142
            os.environ['ST2_AUTH_TOKEN'] = token
143
144
        return client
145
146
    def _get_config_file_options(self, args):
147
        """
148
        Parse the config and return kwargs which can be passed to the Client
149
        constructor.
150
151
        :rtype: ``dict``
152
        """
153
        rc_options = self._parse_config_file(args=args)
154
        result = {}
155
        for kwarg_name, (section, option) in six.iteritems(CONFIG_OPTION_TO_CLIENT_KWARGS_MAP):
156
            result[kwarg_name] = rc_options.get(section, {}).get(option, None)
157
158
        return result
159
160
    def _parse_config_file(self, args):
161
        config_file_path = self._get_config_file_path(args=args)
162
163
        parser = CLIConfigParser(config_file_path=config_file_path, validate_config_exists=False)
164
        result = parser.parse()
165
        return result
166
167
    def _get_config_file_path(self, args):
168
        """
169
        Retrieve path to the CLI configuration file.
170
171
        :rtype: ``str``
172
        """
173
        path = os.environ.get('ST2_CONFIG_FILE', ST2_CONFIG_PATH)
174
175
        if args.config_file:
176
            path = args.config_file
177
178
        path = os.path.abspath(os.path.expanduser(path))
179
        if path != ST2_CONFIG_PATH and not os.path.isfile(path):
180
            raise ValueError('Config "%s" not found' % (path))
181
182
        return path
183
184
    def _get_auth_token(self, client, username, password, cache_token):
185
        """
186
        Retrieve a valid auth token.
187
188
        If caching is enabled, we will first try to retrieve cached token from a
189
        file system. If cached token is expired or not available, we will try to
190
        authenticate using the provided credentials and retrieve a new auth
191
        token.
192
193
        :rtype: ``str``
194
        """
195
        if cache_token:
196
            token = self._get_cached_auth_token(client=client, username=username,
197
                                                password=password)
198
        else:
199
            token = None
200
        if not token:
201
            # Token is either expired or not available
202
            token_obj = self._authenticate_and_retrieve_auth_token(client=client,
203
                                                                   username=username,
204
                                                                   password=password)
205
206
            self._cache_auth_token(token_obj=token_obj)
207
            token = token_obj.token
208
209
        return token
210
211
    def _get_cached_auth_token(self, client, username, password):
212
        """
213
        Retrieve cached auth token from the file in the config directory.
214
215
        :rtype: ``str``
216
        """
217
        if not os.path.isdir(ST2_CONFIG_DIRECTORY):
218
            os.makedirs(ST2_CONFIG_DIRECTORY)
219
220
        cached_token_path = self._get_cached_token_path_for_user(username=username)
221
222
        if not os.access(ST2_CONFIG_DIRECTORY, os.R_OK):
223
            # We don't have read access to the file with a cached token
224
            message = ('Unable to retrieve cached token from "%s" (user %s doesn\'t have read '
225
                       'access to the parent directory). Subsequent requests won\'t use a '
226
                       'cached token meaning they may be slower.' % (cached_token_path,
227
                                                                     os.getlogin()))
228
            self.LOG.warn(message)
229
            return None
230
231
        if not os.path.isfile(cached_token_path):
232
            return None
233
234
        if not os.access(cached_token_path, os.R_OK):
235
            # We don't have read access to the file with a cached token
236
            message = ('Unable to retrieve cached token from "%s" (user %s doesn\'t have read '
237
                       'access to this file). Subsequent requests won\'t use a cached token '
238
                       'meaning they may be slower.' % (cached_token_path, os.getlogin()))
239
            self.LOG.warn(message)
240
            return None
241
242
        # Safety check for too permissive permissions
243
        file_st_mode = oct(os.stat(cached_token_path).st_mode & 0777)
244
        others_st_mode = int(file_st_mode[-1])
245
246
        if others_st_mode >= 4:
247
            # Every user has access to this file which is dangerous
248
            message = ('Permissions (%s) for cached token file "%s" are to permissive. Please '
249
                       'restrict the permissions and make sure only your own user can read '
250
                       'from the file' % (file_st_mode, cached_token_path))
251
            self.LOG.warn(message)
252
253
        with open(cached_token_path) as fp:
254
            data = fp.read()
255
256
        try:
257
            data = json.loads(data)
258
259
            token = data['token']
260
            expire_timestamp = data['expire_timestamp']
261
        except Exception as e:
262
            msg = ('File "%s" with cached token is corrupted or invalid (%s). Please delete '
263
                   ' this file' % (cached_token_path, str(e)))
264
            raise ValueError(msg)
265
266
        now = int(time.time())
267
        if (expire_timestamp - TOKEN_EXPIRATION_GRACE_PERIOD_SECONDS) < now:
268
            self.LOG.debug('Cached token from file "%s" has expired' % (cached_token_path))
269
            # Token has expired
270
            return None
271
272
        self.LOG.debug('Using cached token from file "%s"' % (cached_token_path))
273
        return token
274
275
    def _cache_auth_token(self, token_obj):
276
        """
277
        Cache auth token in the config directory.
278
279
        :param token_obj: Token object.
280
        :type token_obj: ``object``
281
        """
282
        if not os.path.isdir(ST2_CONFIG_DIRECTORY):
283
            os.makedirs(ST2_CONFIG_DIRECTORY)
284
285
        username = token_obj.user
286
        cached_token_path = self._get_cached_token_path_for_user(username=username)
287
288
        if not os.access(ST2_CONFIG_DIRECTORY, os.W_OK):
289
            # We don't have write access to the file with a cached token
290
            message = ('Unable to write token to "%s" (user %s doesn\'t have write '
291
                       'access to the parent directory). Subsequent requests won\'t use a '
292
                       'cached token meaning they may be slower.' % (cached_token_path,
293
                                                                     os.getlogin()))
294
            self.LOG.warn(message)
295
            return None
296
297
        if os.path.isfile(cached_token_path) and not os.access(cached_token_path, os.W_OK):
298
            # We don't have write access to the file with a cached token
299
            message = ('Unable to write token to "%s" (user %s doesn\'t have write '
300
                       'access to this file). Subsequent requests won\'t use a '
301
                       'cached token meaning they may be slower.' % (cached_token_path,
302
                                                                     os.getlogin()))
303
            self.LOG.warn(message)
304
            return None
305
306
        token = token_obj.token
307
        expire_timestamp = parse_isotime(token_obj.expiry)
308
        expire_timestamp = calendar.timegm(expire_timestamp.timetuple())
309
310
        data = {}
311
        data['token'] = token
312
        data['expire_timestamp'] = expire_timestamp
313
        data = json.dumps(data)
314
315
        # Note: We explictly use fdopen instead of open + chmod to avoid a security issue.
316
        # open + chmod are two operations which means that during a short time frame (between
317
        # open and chmod) when file can potentially be read by other users if the default
318
        # permissions used during create allow that.
319
        fd = os.open(cached_token_path, os.O_WRONLY | os.O_CREAT, 0600)
320
        with os.fdopen(fd, 'w') as fp:
321
            fp.write(data)
322
323
        self.LOG.debug('Token has been cached in "%s"' % (cached_token_path))
324
        return True
325
326
    def _authenticate_and_retrieve_auth_token(self, client, username, password):
327
        manager = models.ResourceManager(models.Token, client.endpoints['auth'],
328
                                         cacert=client.cacert, debug=client.debug)
329
        instance = models.Token()
330
        instance = manager.create(instance, auth=(username, password))
331
        return instance
332
333
    def _get_cached_token_path_for_user(self, username):
334
        """
335
        Retrieve cached token path for the provided username.
336
        """
337
        file_name = 'token-%s' % (username)
338
        result = os.path.abspath(os.path.join(ST2_CONFIG_DIRECTORY, file_name))
339
        return result
340
341
    def _print_config(self, args):
342
        config = self._parse_config_file(args=args)
343
344
        for section, options in six.iteritems(config):
345
            print('[%s]' % (section))
346
347
            for name, value in six.iteritems(options):
348
                print('%s = %s' % (name, value))
349
350
    def _print_debug_info(self, args):
351
        # Print client settings
352
        self._print_client_settings(args=args)
353
354
        # Print exception traceback
355
        traceback.print_exc()
356
357
    def _print_client_settings(self, args):
358
        client = self.client
359
360
        if not client:
361
            return
362
363
        config_file_path = self._get_config_file_path(args=args)
364
365
        print('CLI settings:')
366
        print('----------------')
367
        print('Config file path: %s' % (config_file_path))
368
        print('Client settings:')
369
        print('----------------')
370
        print('ST2_BASE_URL: %s' % (client.endpoints['base']))
371
        print('ST2_AUTH_URL: %s' % (client.endpoints['auth']))
372
        print('ST2_API_URL: %s' % (client.endpoints['api']))
373
        print('ST2_STREAM_URL: %s' % (client.endpoints['stream']))
374
        print('ST2_AUTH_TOKEN: %s' % (os.environ.get('ST2_AUTH_TOKEN')))
375
        print('')
376
        print('Proxy settings:')
377
        print('---------------')
378
        print('HTTP_PROXY: %s' % (os.environ.get('HTTP_PROXY', '')))
379
        print('HTTPS_PROXY: %s' % (os.environ.get('HTTPS_PROXY', '')))
380
        print('')
381