Test Failed
Push — master ( e380d0...f5671d )
by W
02:58
created

st2client/st2client/commands/auth.py (2 issues)

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
from __future__ import absolute_import
17
18
import getpass
19
import json
20
import logging
21
import os
22
23
import requests
24
from six.moves.configparser import ConfigParser
25
from six.moves import http_client
26
27
from st2client.base import BaseCLIApp
28
from st2client import config_parser
29
from st2client import models
30
from st2client.commands import resource
31
from st2client.commands.noop import NoopCommand
32
from st2client.exceptions.operations import OperationFailureException
33
from st2client.formatters import table
34
35
36
LOG = logging.getLogger(__name__)
37
38
39
class TokenCreateCommand(resource.ResourceCommand):
40
41
    display_attributes = ['user', 'token', 'expiry']
42
43
    def __init__(self, resource, *args, **kwargs):
44
45
        kwargs['has_token_opt'] = False
46
47
        super(TokenCreateCommand, self).__init__(
48
            resource, kwargs.pop('name', 'create'),
49
            'Authenticate user and acquire access token.',
50
            *args, **kwargs)
51
52
        self.parser.add_argument('username',
53
                                 help='Name of the user to authenticate.')
54
55
        self.parser.add_argument('-p', '--password', dest='password',
56
                                 help='Password for the user. If password is not provided, '
57
                                      'it will be prompted for.')
58
        self.parser.add_argument('-l', '--ttl', type=int, dest='ttl', default=None,
59
                                 help='The life span of the token in seconds. '
60
                                      'Max TTL configured by the admin supersedes this.')
61
        self.parser.add_argument('-t', '--only-token', action='store_true', dest='only_token',
62
                                 default=False,
63
                                 help='On successful authentication, print only token to the '
64
                                      'console.')
65
66
    def run(self, args, **kwargs):
67
        if not args.password:
68
            args.password = getpass.getpass()
69
        instance = self.resource(ttl=args.ttl) if args.ttl else self.resource()
70
        return self.manager.create(instance, auth=(args.username, args.password), **kwargs)
71
72
    def run_and_print(self, args, **kwargs):
73
        instance = self.run(args, **kwargs)
74
75
        if args.only_token:
76
            print(instance.token)
77
        else:
78
            self.print_output(instance, table.PropertyValueTable,
79
                              attributes=self.display_attributes, json=args.json, yaml=args.yaml)
80
81
82
class LoginCommand(resource.ResourceCommand):
83
    display_attributes = ['user', 'token', 'expiry']
84
85
    def __init__(self, resource, *args, **kwargs):
86
87
        kwargs['has_token_opt'] = False
88
89
        super(LoginCommand, self).__init__(
90
            resource, kwargs.pop('name', 'create'),
91
            'Authenticate user, acquire access token, and update CLI config directory',
92
            *args, **kwargs)
93
94
        self.parser.add_argument('username',
95
                                 help='Name of the user to authenticate.')
96
97
        self.parser.add_argument('-p', '--password', dest='password',
98
                                 help='Password for the user. If password is not provided, '
99
                                      'it will be prompted for.')
100
        self.parser.add_argument('-l', '--ttl', type=int, dest='ttl', default=None,
101
                                 help='The life span of the token in seconds. '
102
                                      'Max TTL configured by the admin supersedes this.')
103
        self.parser.add_argument('-w', '--write-password', action='store_true', default=False,
104
                                 dest='write_password',
105
                                 help='Write the password in plain text to the config file '
106
                                      '(default is to omit it)')
107
108
    def run(self, args, **kwargs):
109
110
        if not args.password:
111
            args.password = getpass.getpass()
112
        instance = self.resource(ttl=args.ttl) if args.ttl else self.resource()
113
114
        cli = BaseCLIApp()
115
116
        # Determine path to config file
117
        try:
118
            config_file = cli._get_config_file_path(args)
119
        except ValueError:
120
            # config file not found in args or in env, defaulting
121
            config_file = config_parser.ST2_CONFIG_PATH
122
123
        # Retrieve token
124
        manager = self.manager.create(instance, auth=(args.username, args.password), **kwargs)
125
        cli._cache_auth_token(token_obj=manager)
126
127
        # Update existing configuration with new credentials
128
        config = ConfigParser()
129
        config.read(config_file)
130
131
        # Modify config (and optionally populate with password)
132
        if not config.has_section('credentials'):
133
            config.add_section('credentials')
134
135
        config.set('credentials', 'username', args.username)
136
        if args.write_password:
137
            config.set('credentials', 'password', args.password)
138
        else:
139
            # Remove any existing password from config
140
            config.remove_option('credentials', 'password')
141
142
        config_existed = os.path.exists(config_file)
143
        with open(config_file, 'w') as cfg_file_out:
144
            config.write(cfg_file_out)
145
        # If we created the config file, correct the permissions
146
        if not config_existed:
147
            os.chmod(config_file, 0o660)
148
149
        return manager
150
151
    def run_and_print(self, args, **kwargs):
152
        try:
153
            self.run(args, **kwargs)
154
        except Exception as e:
155
            print('Failed to log in as %s: %s' % (args.username, str(e)))
156
            if self.app.client.debug:
157
                raise
158
159
            return
160
161
        print('Logged in as %s' % (args.username))
162
163
        if not args.write_password:
164
            # Note: Client can't depend and import from common so we need to hard-code this
165
            # default value
166
            token_expire_hours = 24
167
168
            print('')
169
            print('Note: You didn\'t use --write-password option so the password hasn\'t been '
170
                  'stored in the client config and you will need to login again in %s hours when '
171
                  'the auth token expires.' % (token_expire_hours))
172
            print('As an alternative, you can run st2 login command with the "--write-password" '
173
                  'flag, but keep it mind this will cause it to store the password in plain-text '
174
                  'in the client config file (~/.st2/config).')
175
176
177
class WhoamiCommand(resource.ResourceCommand):
178
    display_attributes = ['user', 'token', 'expiry']
179
180
    def __init__(self, resource, *args, **kwargs):
181
182
        kwargs['has_token_opt'] = False
183
184
        super(WhoamiCommand, self).__init__(
185
            resource, kwargs.pop('name', 'create'),
186
            'Display the currently authenticated user',
187
            *args, **kwargs)
188
189
    def run(self, args, **kwargs):
190
        user_info = self.app.client.get_user_info(**kwargs)
191
        return user_info
192
193
    def run_and_print(self, args, **kwargs):
194
        try:
195
            user_info = self.run(args, **kwargs)
196
        except Exception as e:
197
            response = getattr(e, 'response', None)
198
            status_code = getattr(response, 'status_code', None)
199
            is_unathorized_error = (status_code == http_client.UNAUTHORIZED)
200
201
            if response and is_unathorized_error:
202
                print('Not authenticated')
203
            else:
204
                print('Unable to retrieve currently logged-in user')
205
206
            if self.app.client.debug:
207
                raise
208
209
            return
210
211
        print('Currently logged in as "%s".' % (user_info['username']))
212
        print('')
213
        print('Authentication method: %s' % (user_info['authentication']['method']))
214
215
        if user_info['authentication']['method'] == 'authentication token':
216
            print('Authentication token expire time: %s' %
217
                  (user_info['authentication']['token_expire']))
218
219
        print('')
220
        print('RBAC:')
221
        print(' - Enabled: %s' % (user_info['rbac']['enabled']))
222
        print(' - Roles: %s' % (', '.join(user_info['rbac']['roles'])))
223
224
225
class ApiKeyBranch(resource.ResourceBranch):
226
227
    def __init__(self, description, app, subparsers, parent_parser=None):
228
        super(ApiKeyBranch, self).__init__(
229
            models.ApiKey, description, app, subparsers,
230
            parent_parser=parent_parser,
231
            commands={
232
                'list': ApiKeyListCommand,
233
                'get': ApiKeyGetCommand,
234
                'create': ApiKeyCreateCommand,
235
                'update': NoopCommand,
236
                'delete': ApiKeyDeleteCommand
237
            })
238
239
        self.commands['enable'] = ApiKeyEnableCommand(self.resource, self.app, self.subparsers)
240
        self.commands['disable'] = ApiKeyDisableCommand(self.resource, self.app, self.subparsers)
241
        self.commands['load'] = ApiKeyLoadCommand(self.resource, self.app, self.subparsers)
242
243
244
class ApiKeyListCommand(resource.ResourceListCommand):
245
    detail_display_attributes = ['all']
246
    display_attributes = ['id', 'user', 'metadata']
247
248
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
249
        super(ApiKeyListCommand, self).__init__(resource, *args, **kwargs)
250
251
        self.parser.add_argument('-u', '--user', type=str,
252
                                 help='Only return ApiKeys belonging to the provided user')
253
        self.parser.add_argument('-d', '--detail', action='store_true',
254
                                 help='Full list of attributes.')
255
        self.parser.add_argument('--show-secrets', action='store_true',
256
                                 help='Full list of attributes.')
257
258
    @resource.add_auth_token_to_kwargs_from_cli
259
    def run(self, args, **kwargs):
260
        filters = {}
261
        filters['user'] = args.user
262
        filters.update(**kwargs)
263
        # show_secrets is not a filter but a query param. There is some special
264
        # handling for filters in the get method which reuqires this odd hack.
265
        if args.show_secrets:
266
            params = filters.get('params', {})
267
            params['show_secrets'] = True
268
            filters['params'] = params
269
        return self.manager.get_all(**filters)
270
271
    def run_and_print(self, args, **kwargs):
272
        instances = self.run(args, **kwargs)
273
        attr = self.detail_display_attributes if args.detail else args.attr
274
        self.print_output(instances, table.MultiColumnTable,
275
                          attributes=attr, widths=args.width,
276
                          json=args.json, yaml=args.yaml)
277
278
279
class ApiKeyGetCommand(resource.ResourceGetCommand):
280
    display_attributes = ['all']
281
    attribute_display_order = ['id', 'user', 'metadata']
282
283
    pk_argument_name = 'key_or_id'  # name of the attribute which stores resource PK
284
285
286
class ApiKeyCreateCommand(resource.ResourceCommand):
287
288
    def __init__(self, resource, *args, **kwargs):
0 ignored issues
show
Comprehensibility Bug introduced by
resource is re-defining a name which is already available in the outer-scope (previously defined on line 30).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
289
        super(ApiKeyCreateCommand, self).__init__(
290
            resource, 'create', 'Create a new %s.' % resource.get_display_name().lower(),
291
            *args, **kwargs)
292
293
        self.parser.add_argument('-u', '--user', type=str,
294
                                 help='User for which to create API Keys.',
295
                                 default='')
296
        self.parser.add_argument('-m', '--metadata', type=json.loads,
297
                                 help='Optional metadata to associate with the API Keys.',
298
                                 default={})
299
        self.parser.add_argument('-k', '--only-key', action='store_true', dest='only_key',
300
                                 default=False,
301
                                 help='Only print API Key to the console on creation.')
302
303
    @resource.add_auth_token_to_kwargs_from_cli
304
    def run(self, args, **kwargs):
305
        data = {}
306
        if args.user:
307
            data['user'] = args.user
308
        if args.metadata:
309
            data['metadata'] = args.metadata
310
        instance = self.resource.deserialize(data)
311
        return self.manager.create(instance, **kwargs)
312
313
    def run_and_print(self, args, **kwargs):
314
        try:
315
            instance = self.run(args, **kwargs)
316
            if not instance:
317
                raise Exception('Server did not create instance.')
318
        except Exception as e:
319
            message = e.message or str(e)
320
            print('ERROR: %s' % (message))
321
            raise OperationFailureException(message)
322
        if args.only_key:
323
            print(instance.key)
324
        else:
325
            self.print_output(instance, table.PropertyValueTable,
326
                              attributes=['all'], json=args.json, yaml=args.yaml)
327
328
329
class ApiKeyLoadCommand(resource.ResourceCommand):
330
331
    def __init__(self, resource, *args, **kwargs):
332
        super(ApiKeyLoadCommand, self).__init__(
333
            resource, 'load', 'Load %s from a file.' % resource.get_display_name().lower(),
334
            *args, **kwargs)
335
336
        self.parser.add_argument('file',
337
                                 help=('JSON/YAML file containing the %s(s) to load.'
338
                                       % resource.get_display_name().lower()),
339
                                 default='')
340
341
        self.parser.add_argument('-w', '--width', nargs='+', type=int,
342
                                 default=None,
343
                                 help=('Set the width of columns in output.'))
344
345
    @resource.add_auth_token_to_kwargs_from_cli
346
    def run(self, args, **kwargs):
347
        resources = resource.load_meta_file(args.file)
348
        if not resources:
349
            print('No %s found in %s.' % (self.resource.get_display_name().lower(), args.file))
350
            return None
351
        if not isinstance(resources, list):
352
            resources = [resources]
353
        instances = []
354
        for res in resources:
355
            # pick only the meaningful properties.
356
            data = {
357
                'user': res['user'],  # required
358
                'key_hash': res['key_hash'],  # required
359
                'metadata': res.get('metadata', {}),
360
                'enabled': res.get('enabled', False)
361
            }
362
363
            if 'id' in res:
364
                data['id'] = res['id']
365
366
            instance = self.resource.deserialize(data)
367
368
            try:
369
                result = self.manager.update(instance, **kwargs)
370
            except requests.exceptions.HTTPError as e:
371
                if e.response.status_code == http_client.NOT_FOUND:
372
                    instance = self.resource.deserialize(data)
373
                    # Key doesn't exist yet, create it instead
374
                    result = self.manager.create(instance, **kwargs)
375
                else:
376
                    raise e
377
378
            instances.append(result)
379
        return instances
380
381
    def run_and_print(self, args, **kwargs):
382
        instances = self.run(args, **kwargs)
383
        if instances:
384
            self.print_output(instances, table.MultiColumnTable,
385
                              attributes=ApiKeyListCommand.display_attributes,
386
                              widths=args.width,
387
                              json=args.json, yaml=args.yaml)
388
389
390
class ApiKeyDeleteCommand(resource.ResourceDeleteCommand):
391
    pk_argument_name = 'key_or_id'  # name of the attribute which stores resource PK
392
393
394
class ApiKeyEnableCommand(resource.ResourceEnableCommand):
395
    pk_argument_name = 'key_or_id'  # name of the attribute which stores resource PK
396
397
398
class ApiKeyDisableCommand(resource.ResourceDisableCommand):
399
    pk_argument_name = 'key_or_id'  # name of the attribute which stores resource PK
400