Added seed code for access-management.
[ta/access-management.git] / src / access_management / cli / cli.py
1 # Copyright 2019 Nokia
2
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #     http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # pylint: disable=line-too-long, too-few-public-methods
16
17 import sys
18 from copy import deepcopy
19 from hostcli.helper import ListerHelper, ShowOneHelper, CommandHelper
20 import getpass
21 import os
22
23
24 API_VERSION =       'v1'
25 RESOURCE_PREFIX =   'am/%s/' % API_VERSION
26 DEFAULTPROJECTID =  'default_project_id'
27 DOMAINID =          'domain_id'
28 ENABLED =           'enabled'
29 UUID =              'id'
30 USER =              'user'
31 OWNUUID =           'ownid'
32 USERS =             'users'
33 LINKS =             'links'
34 USERNAME =          'username'
35 NAME =              'name'
36 OPTIONS =           'options'
37 PASSWORDEXP =       'password_expires_at'
38 PASSWORD =          'password'
39 EMAIL =             'email'
40 ROLENAME =          'role_name'
41 ROLEDESC =          'desc'
42 ROLES =             'roles'
43 ISSERVICE =         'is_service'
44 ISCHROOT =          'is_chroot'
45 NEWPASSWORD =       'npassword'
46 OLDPASSWORD =       'opassword'
47 PROJECTID =         'project_id'
48 PROJECT =           'project'
49 RESOURCEPATH =      'res_path'
50 RESOURCEOP =        'res_op'
51 PERMISSIONNAME =    'permission_name'
52 PERMISSIONRES =     'resources'
53 PUBSSHKEY =         'key'
54 SORT =              'sort'
55
56
57 FIELDMAP = {
58     DEFAULTPROJECTID:   {'display': 'Default-Project-ID',
59                          'help': 'The ID of the default project for the user.'},
60     DOMAINID:           {'display': 'Domain-ID',
61                          'help': 'The ID of the domain.'},
62     ENABLED:            {'display': 'Enabled',
63                          'help': 'Whether the user is able to log in or not.'},
64     UUID:               {'display': 'User-ID',
65                          'help': 'The user ID.'},
66     USER:               {'display': 'User',
67                          'help': 'The user ID, or user name.'},
68     USERS:              {'display': 'User-IDs',
69                          'help': 'List of the user IDs.'},
70     LINKS:              {'display': 'Links',
71                          'help': 'The links for the user resource.'},
72     USERNAME:           {'help': 'The user name.'},
73     NAME:               {'display': 'User-Name',
74                          'help': 'The user name.'},
75     OPTIONS:            {'display': 'Options',
76                          'help': 'Options'},
77     PASSWORDEXP:        {'display': 'Password-Expires',
78                          'help': 'The date and time when the password expires. The time zone is UTC. A null value indicates that the password never expires.'},
79     PASSWORD:           {'default': '',
80                          'help': 'The password'},
81     EMAIL:              {'display': 'E-mail',
82                          'help': 'The email'},
83     ROLENAME:           {'display': 'Role',
84                          'help': 'The role name.'},
85     ROLEDESC:           {'display': 'Description',
86                          'help': 'The description of the role. It should be enclosed in apostrophes if it contains spaces'},
87     ROLES:              {'display': 'Roles',
88                          'help': 'The roles of the user.'},
89     ISSERVICE:          {'display': 'Immutable',
90                          'help': 'Whether the role is a service role. It is non-modifiable.'},
91     ISCHROOT:           {'display': 'Log File Access right',
92                          'help': 'Permission to use chroot file transfer.'},
93     NEWPASSWORD:        {'default': '',
94                          'help': 'The new password.'},
95     OLDPASSWORD:        {'default': '',
96                          'help': 'The old password'},
97     PROJECTID:          {'help': 'The ID of the project'},
98     PROJECT:            {'help': 'The ID of the project'},
99     RESOURCEPATH:       {'help': 'Resource path is the corresponding REST API URL.'},
100     RESOURCEOP:         {'help': 'The resource operation'},
101     PERMISSIONNAME:     {'display': 'Permission-Name',
102                         'help': 'Existing operations for the REST API endpoint.'},
103     PERMISSIONRES:      {'display': 'Permission-Resources',
104                         'help': 'Path of the REST API endpoint.'},
105     PUBSSHKEY:          {'help': 'The public ssh key string itself (not a key file).'},
106     SORT:               {'help': 'Comma-separated list of sort keys and directions in the form of <key>[:<asc|desc>]. The direction defaults to ascending if not specified. '
107                                  'Sort keys are the case sensitive column names in the command output table. For this command they are: User-ID, User-Name, Enabled and Password-Expires.'}
108 }
109
110 PASSWORDPOLICY_DOCSTRING = """
111     The password must have a minimum length of 8 characters (maximum is 255 characters).
112     The allowed characters are lower case letters (a-z), upper case letters (A-Z), digits (0-9), and special characters (.,:;/(){}<>~\!?@#$%^&*_=+-).
113     The password must contain at least one upper case letter, one digit and one special character.
114     The new password is always checked against a password dictionary and it cannot be the same with any of the last 12 passwords already used."""
115
116
117 def password_policy_docstring(a):
118     a.__doc__ = a.__doc__.replace("%PASSWORDPOLICY_DOCSTRING%", PASSWORDPOLICY_DOCSTRING)
119     return a
120
121
122 class AmCliLister(ListerHelper):
123     """Helper class for Lister"""
124     def __init__(self, app, app_args, cmd_name=None):
125         super(AmCliLister, self).__init__(app, app_args, cmd_name)
126         self.fieldmap = deepcopy(FIELDMAP)
127         self.resource_prefix = RESOURCE_PREFIX
128
129
130 class AmCliShowOne(ShowOneHelper):
131     """Helper class for ShowOne"""
132     def __init__(self, app, app_args, cmd_name=None):
133         super(AmCliShowOne, self).__init__(app, app_args, cmd_name)
134         self.fieldmap = deepcopy(FIELDMAP)
135         self.resource_prefix = RESOURCE_PREFIX
136
137
138 class AmCliCommand(CommandHelper):
139     """Helper class for Command"""
140     def __init__(self, app, app_args, cmd_name=None):
141         super(AmCliCommand, self).__init__(app, app_args, cmd_name)
142         self.fieldmap = deepcopy(FIELDMAP)
143         self.resource_prefix = RESOURCE_PREFIX
144
145
146 @password_policy_docstring
147 class CreateNewUser(AmCliCommand):
148     """A command for creating new user in keystone.
149     The password is prompted if not given as parameter.
150     %PASSWORDPOLICY_DOCSTRING%"""
151     def __init__(self, app, app_args, cmd_name=None):
152         super(CreateNewUser, self).__init__(app, app_args, cmd_name)
153         self.usebody = True
154         self.operation = 'post'
155         self.endpoint = 'users'
156         self.mandatory_positional = True
157         self.positional_count = 1
158         self.arguments = [USERNAME, EMAIL, PASSWORD, PROJECT]
159         self.message = 'User created. The UUID is ##id'
160
161     def take_action(self, parsed_args):
162         try:
163             if parsed_args.password == '':
164                 password1 = getpass.getpass(prompt='Password: ')
165                 password2 = getpass.getpass(prompt='Password again: ')
166                 if password1 == password2:
167                     parsed_args.password = password1
168                 else:
169                     raise Exception('New passwords do not match')
170             result = self.send_receive(self.app, parsed_args)
171             if self.message:
172                 self.app.stdout.write(ResetUserPassword.construct_message(self.message, result))
173         except Exception as exp:
174             self.app.stderr.write('Failed with error %s\n' % str(exp))
175             sys.exit(1)
176
177
178 class DeleteUsers(AmCliCommand):
179     """A command for deleting one or more existing users."""
180     def __init__(self, app, app_args, cmd_name=None):
181         super(DeleteUsers, self).__init__(app, app_args, cmd_name)
182         self.operation = 'delete'
183         self.endpoint = 'users'
184         self.mandatory_positional = True
185         self.positional_count = 1
186         self.arguments = [USER]
187         self.message = 'User deleted.'
188
189
190 class ListUsers(AmCliLister):
191     """A command for listing existing users."""
192     def __init__(self, app, app_args, cmd_name=None):
193         super(ListUsers, self).__init__(app, app_args, cmd_name)
194         self.operation = 'get'
195         self.endpoint = 'users'
196         self.positional_count = 0
197         self.arguments = [SORT]
198         self.columns = [UUID, NAME, ENABLED, PASSWORDEXP]
199         self.default_sort = [NAME, 'asc']
200
201
202 @password_policy_docstring
203 class ChangeUserPassword(AmCliCommand):
204     """A command for changing the current user password (i.e. own password).
205     The old and new passwords are prompted if not given as parameter.
206     %PASSWORDPOLICY_DOCSTRING%"""
207     def __init__(self, app, app_args, cmd_name=None):
208         super(ChangeUserPassword, self).__init__(app, app_args, cmd_name)
209         self.usebody = True
210         self.operation = 'post'
211         self.endpoint = 'users/ownpasswords'
212         #self.mandatory_positional = False
213         self.no_positional = True
214         self.arguments = [OLDPASSWORD, NEWPASSWORD]
215         self.message = 'Your password has been changed.'
216         self.auth_required = False
217
218     def take_action(self, parsed_args):
219         try:
220             if parsed_args.opassword == '':
221                 parsed_args.opassword = getpass.getpass(prompt='Old password: ')
222             if parsed_args.npassword == '':
223                 npassword1 = getpass.getpass(prompt='New password: ')
224                 npassword2 = getpass.getpass(prompt='New password again: ')
225                 if npassword1 == npassword2:
226                     parsed_args.npassword = npassword1
227                 else:
228                     raise Exception('New passwords do not match')
229             parsed_args.username = os.environ['OS_USERNAME']
230             self.arguments.append(USERNAME)
231             result = self.send_receive(self.app, parsed_args)
232             if self.message:
233                 self.app.stdout.write(ResetUserPassword.construct_message(self.message, result))
234         except Exception as exp:
235             self.app.stderr.write('Failed with error %s\n' % str(exp))
236             sys.exit(1)
237
238
239 @password_policy_docstring
240 class ResetUserPassword(AmCliCommand):
241     """A command for user administrators for changing other user's password.
242     Own password cannot be changed with this command.
243     Note that user management admin role is required.
244     The new password is prompted if not given as parameter.
245     %PASSWORDPOLICY_DOCSTRING%"""
246     def __init__(self, app, app_args, cmd_name=None):
247         super(ResetUserPassword, self).__init__(app, app_args, cmd_name)
248         self.usebody = True
249         self.operation = 'post'
250         self.endpoint = 'users/passwords'
251         self.mandatory_positional = True
252         self.positional_count = 1
253         self.arguments = [USER, NEWPASSWORD]
254         self.message = 'Password has been reset for the user.'
255
256     def take_action(self, parsed_args):
257         try:
258             if parsed_args.npassword == '':
259                 npassword1 = getpass.getpass(prompt='New password: ')
260                 npassword2 = getpass.getpass(prompt='New password again: ')
261                 if npassword1 == npassword2:
262                     parsed_args.npassword = npassword1
263                 else:
264                     raise Exception('New passwords do not match')
265             result = self.send_receive(self.app, parsed_args)
266             if self.message:
267                 self.app.stdout.write(ResetUserPassword.construct_message(self.message, result))
268         except Exception as exp:
269             self.app.stderr.write('Failed with error %s\n' % str(exp))
270             sys.exit(1)
271
272
273 class SetUserParameters(AmCliCommand):
274     """A command for setting user parameters."""
275     def __init__(self, app, app_args, cmd_name=None):
276         super(SetUserParameters, self).__init__(app, app_args, cmd_name)
277         self.operation = 'post'
278         self.endpoint = 'users/parameters'
279         self.mandatory_positional = True
280         self.positional_count = 1
281         self.arguments = [USER, PROJECTID, EMAIL]
282         self.message = 'Parameter of the user is changed.'
283
284
285 class ShowUserDetails(AmCliShowOne):
286     """A command for displaying the details of a user."""
287     def __init__(self, app, app_args, cmd_name=None):
288         super(ShowUserDetails, self).__init__(app, app_args, cmd_name)
289         self.operation = 'get'
290         self.endpoint = 'users/details'
291         self.mandatory_positional = True
292         self.positional_count = 1
293         self.arguments = [USER]
294         self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES]
295
296
297 class ShowUserOwnDetails(AmCliShowOne):
298     """A command for displaying the details of a user."""
299     def __init__(self, app, app_args, cmd_name=None):
300         super(ShowUserOwnDetails, self).__init__(app, app_args, cmd_name)
301         self.operation = 'get'
302         self.endpoint = 'users/owndetails'
303         self.mandatory_positional = True
304         self.positional_count = 0
305         self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES]
306
307
308 class AddRoleForUser(AmCliCommand):
309     """A command for adding role to a user."""
310     def __init__(self, app, app_args, cmd_name=None):
311         super(AddRoleForUser, self).__init__(app, app_args, cmd_name)
312         self.operation = 'post'
313         self.endpoint = 'users/roles'
314         self.mandatory_positional = True
315         self.positional_count = 2
316         self.arguments = [USER, ROLENAME]
317         self.message = 'Role has been added to the user.'
318
319
320 class RemoveRoleFromUser(AmCliCommand):
321     """A command for removing role from a user."""
322     def __init__(self, app, app_args, cmd_name=None):
323         super(RemoveRoleFromUser, self).__init__(app, app_args, cmd_name)
324         self.operation = 'delete'
325         self.endpoint = 'users/roles'
326         self.mandatory_positional = True
327         self.positional_count = 2
328         self.arguments = [USER, ROLENAME]
329         self.message = 'Role has been removed from the user.'
330
331
332 class LockUser(AmCliCommand):
333     """A command for locking an account."""
334     def __init__(self, app, app_args, cmd_name=None):
335         super(LockUser, self).__init__(app, app_args, cmd_name)
336         self.operation = 'post'
337         self.endpoint = 'users/locks'
338         self.mandatory_positional = True
339         self.positional_count = 1
340         self.arguments = [USER]
341         self.message = 'User has been locked.'
342
343
344 class UnlockUser(AmCliCommand):
345     """A command for enabling a locked account."""
346     def __init__(self, app, app_args, cmd_name=None):
347         super(UnlockUser, self).__init__(app, app_args, cmd_name)
348         self.operation = 'delete'
349         self.endpoint = 'users/locks'
350         self.mandatory_positional = True
351         self.positional_count = 1
352         self.arguments = [USER]
353         self.message = 'User has been enabled.'
354
355
356 class CreateNewRole(AmCliCommand):
357     """A command for creating a new role."""
358     def __init__(self, app, app_args, cmd_name=None):
359         super(CreateNewRole, self).__init__(app, app_args, cmd_name)
360         self.operation = 'post'
361         self.endpoint = 'roles'
362         self.mandatory_positional = True
363         self.positional_count = 1
364         self.arguments = [ROLENAME, ROLEDESC]
365         self.message = 'Role has been created.'
366
367
368 class ModifyRole(AmCliCommand):
369     """A command for modifying an existing role."""
370     def __init__(self, app, app_args, cmd_name=None):
371         super(ModifyRole, self).__init__(app, app_args, cmd_name)
372         self.operation = 'put'
373         self.endpoint = 'roles'
374         self.mandatory_positional = True
375         self.positional_count = 1
376         self.arguments = [ROLENAME, ROLEDESC]
377         self.message = 'Role has been modified.'
378
379
380 class DeleteRole(AmCliCommand):
381     """A command for deleting one or more existing roles."""
382     def __init__(self, app, app_args, cmd_name=None):
383         super(DeleteRole, self).__init__(app, app_args, cmd_name)
384         self.operation = 'delete'
385         self.endpoint = 'roles'
386         self.mandatory_positional = True
387         self.positional_count = 1
388         self.arguments = [ROLENAME]
389         self.message = 'Role has been deleted.'
390
391
392 class ListRoles(AmCliLister):
393     """A command for listing existing roles. Openstack roles won't be listed."""
394     def __init__(self, app, app_args, cmd_name=None):
395         super(ListRoles, self).__init__(app, app_args, cmd_name)
396         self.operation = 'get'
397         self.endpoint = 'roles'
398         self.positional_count = 0
399         self.arguments = [SORT]
400         self.columns = [ROLENAME, ROLEDESC, ISSERVICE, ISCHROOT]
401         self.default_sort = [ROLENAME, 'asc']
402
403
404 class ShowRoleDetails(AmCliLister):
405     """A command for displaying the details of a role."""
406     def __init__(self, app, app_args, cmd_name=None):
407         super(ShowRoleDetails, self).__init__(app, app_args, cmd_name)
408         self.operation = 'get'
409         self.endpoint = 'roles/details'
410         self.mandatory_positional = True
411         self.positional_count = 1
412         self.arguments = [ROLENAME]
413         self.columns = [PERMISSIONNAME, PERMISSIONRES]
414
415
416 class ListUsersOfRole(AmCliLister):
417     """A command for listing the users of a role."""
418     def __init__(self, app, app_args, cmd_name=None):
419         super(ListUsersOfRole, self).__init__(app, app_args, cmd_name)
420         self.operation = 'get'
421         self.endpoint = 'roles/users'
422         self.mandatory_positional = True
423         self.positional_count = 1
424         self.arguments = [ROLENAME]
425         self.columns = [ROLENAME, USERS]
426
427
428 class AddPermissionToRole(AmCliCommand):
429     """A command for adding a new permission to a role."""
430     def __init__(self, app, app_args, cmd_name=None):
431         super(AddPermissionToRole, self).__init__(app, app_args, cmd_name)
432         self.operation = 'post'
433         self.endpoint = 'roles/permissions'
434         self.mandatory_positional = True
435         self.positional_count = 3
436         self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP]
437         self.message = 'New permission added to role.'
438
439
440 class RemovePermissionFromRole(AmCliCommand):
441     """A command for removing a permission from a role."""
442     def __init__(self, app, app_args, cmd_name=None):
443         super(RemovePermissionFromRole, self).__init__(app, app_args, cmd_name)
444         self.operation = 'delete'
445         self.endpoint = 'roles/permissions'
446         self.mandatory_positional = True
447         self.positional_count = 3
448         self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP]
449         self.message = 'Permission deleted from role.'
450
451
452 class ListPermissions(AmCliLister):
453     """A command for listing all the permissions and endpoints."""
454     def __init__(self, app, app_args, cmd_name=None):
455         super(ListPermissions, self).__init__(app, app_args, cmd_name)
456         self.operation = 'get'
457         self.endpoint = 'permissions'
458         self.positional_count = 0
459         self.arguments = [SORT]
460         self.columns = [PERMISSIONNAME, PERMISSIONRES]
461         self.default_sort = [PERMISSIONNAME, 'asc']
462
463
464 class AddKey(AmCliCommand):
465     """A command for adding a public ssh key to a user."""
466     def __init__(self, app, app_args, cmd_name=None):
467         super(AddKey, self).__init__(app, app_args, cmd_name)
468         self.operation = 'post'
469         self.endpoint = 'users/keys'
470         self.mandatory_positional = True
471         self.positional_count = 2
472         self.arguments = [USER, PUBSSHKEY]
473         self.message = 'Key added to the user.'
474
475
476 class RemoveKey(AmCliCommand):
477     """A command for removing a public ssh key from a user."""
478     def __init__(self, app, app_args, cmd_name=None):
479         super(RemoveKey, self).__init__(app, app_args, cmd_name)
480         self.operation = 'delete'
481         self.endpoint = 'users/keys'
482         self.mandatory_positional = True
483         self.positional_count = 1
484         self.arguments = [USER]
485         self.message = 'Key removed from the user.'