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): |
||
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): |
||
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): |
||
0 ignored issues
–
show
|
|||
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 |
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: