X-Git-Url: https://gerrit.akraino.org/r/gitweb?p=ta%2Faccess-management.git;a=blobdiff_plain;f=src%2Faccess_management%2Fcli%2Fcli.py;fp=src%2Faccess_management%2Fcli%2Fcli.py;h=15b27d96b4a7e969b14663f4d226cf96e6485083;hp=0000000000000000000000000000000000000000;hb=d37b9ab19ff6f50b9c1746784623b3dd328ab525;hpb=0205adc63d3bba479a24db85d2d4bfdba0411876 diff --git a/src/access_management/cli/cli.py b/src/access_management/cli/cli.py new file mode 100644 index 0000000..15b27d9 --- /dev/null +++ b/src/access_management/cli/cli.py @@ -0,0 +1,485 @@ +# Copyright 2019 Nokia + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=line-too-long, too-few-public-methods + +import sys +from copy import deepcopy +from hostcli.helper import ListerHelper, ShowOneHelper, CommandHelper +import getpass +import os + + +API_VERSION = 'v1' +RESOURCE_PREFIX = 'am/%s/' % API_VERSION +DEFAULTPROJECTID = 'default_project_id' +DOMAINID = 'domain_id' +ENABLED = 'enabled' +UUID = 'id' +USER = 'user' +OWNUUID = 'ownid' +USERS = 'users' +LINKS = 'links' +USERNAME = 'username' +NAME = 'name' +OPTIONS = 'options' +PASSWORDEXP = 'password_expires_at' +PASSWORD = 'password' +EMAIL = 'email' +ROLENAME = 'role_name' +ROLEDESC = 'desc' +ROLES = 'roles' +ISSERVICE = 'is_service' +ISCHROOT = 'is_chroot' +NEWPASSWORD = 'npassword' +OLDPASSWORD = 'opassword' +PROJECTID = 'project_id' +PROJECT = 'project' +RESOURCEPATH = 'res_path' +RESOURCEOP = 'res_op' +PERMISSIONNAME = 'permission_name' +PERMISSIONRES = 'resources' +PUBSSHKEY = 'key' +SORT = 'sort' + + +FIELDMAP = { + DEFAULTPROJECTID: {'display': 'Default-Project-ID', + 'help': 'The ID of the default project for the user.'}, + DOMAINID: {'display': 'Domain-ID', + 'help': 'The ID of the domain.'}, + ENABLED: {'display': 'Enabled', + 'help': 'Whether the user is able to log in or not.'}, + UUID: {'display': 'User-ID', + 'help': 'The user ID.'}, + USER: {'display': 'User', + 'help': 'The user ID, or user name.'}, + USERS: {'display': 'User-IDs', + 'help': 'List of the user IDs.'}, + LINKS: {'display': 'Links', + 'help': 'The links for the user resource.'}, + USERNAME: {'help': 'The user name.'}, + NAME: {'display': 'User-Name', + 'help': 'The user name.'}, + OPTIONS: {'display': 'Options', + 'help': 'Options'}, + PASSWORDEXP: {'display': 'Password-Expires', + 'help': 'The date and time when the password expires. The time zone is UTC. A null value indicates that the password never expires.'}, + PASSWORD: {'default': '', + 'help': 'The password'}, + EMAIL: {'display': 'E-mail', + 'help': 'The email'}, + ROLENAME: {'display': 'Role', + 'help': 'The role name.'}, + ROLEDESC: {'display': 'Description', + 'help': 'The description of the role. It should be enclosed in apostrophes if it contains spaces'}, + ROLES: {'display': 'Roles', + 'help': 'The roles of the user.'}, + ISSERVICE: {'display': 'Immutable', + 'help': 'Whether the role is a service role. It is non-modifiable.'}, + ISCHROOT: {'display': 'Log File Access right', + 'help': 'Permission to use chroot file transfer.'}, + NEWPASSWORD: {'default': '', + 'help': 'The new password.'}, + OLDPASSWORD: {'default': '', + 'help': 'The old password'}, + PROJECTID: {'help': 'The ID of the project'}, + PROJECT: {'help': 'The ID of the project'}, + RESOURCEPATH: {'help': 'Resource path is the corresponding REST API URL.'}, + RESOURCEOP: {'help': 'The resource operation'}, + PERMISSIONNAME: {'display': 'Permission-Name', + 'help': 'Existing operations for the REST API endpoint.'}, + PERMISSIONRES: {'display': 'Permission-Resources', + 'help': 'Path of the REST API endpoint.'}, + PUBSSHKEY: {'help': 'The public ssh key string itself (not a key file).'}, + SORT: {'help': 'Comma-separated list of sort keys and directions in the form of [:]. The direction defaults to ascending if not specified. ' + '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.'} +} + +PASSWORDPOLICY_DOCSTRING = """ + The password must have a minimum length of 8 characters (maximum is 255 characters). + The allowed characters are lower case letters (a-z), upper case letters (A-Z), digits (0-9), and special characters (.,:;/(){}<>~\!?@#$%^&*_=+-). + The password must contain at least one upper case letter, one digit and one special character. + 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.""" + + +def password_policy_docstring(a): + a.__doc__ = a.__doc__.replace("%PASSWORDPOLICY_DOCSTRING%", PASSWORDPOLICY_DOCSTRING) + return a + + +class AmCliLister(ListerHelper): + """Helper class for Lister""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliLister, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +class AmCliShowOne(ShowOneHelper): + """Helper class for ShowOne""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliShowOne, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +class AmCliCommand(CommandHelper): + """Helper class for Command""" + def __init__(self, app, app_args, cmd_name=None): + super(AmCliCommand, self).__init__(app, app_args, cmd_name) + self.fieldmap = deepcopy(FIELDMAP) + self.resource_prefix = RESOURCE_PREFIX + + +@password_policy_docstring +class CreateNewUser(AmCliCommand): + """A command for creating new user in keystone. + The password is prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(CreateNewUser, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USERNAME, EMAIL, PASSWORD, PROJECT] + self.message = 'User created. The UUID is ##id' + + def take_action(self, parsed_args): + try: + if parsed_args.password == '': + password1 = getpass.getpass(prompt='Password: ') + password2 = getpass.getpass(prompt='Password again: ') + if password1 == password2: + parsed_args.password = password1 + else: + raise Exception('New passwords do not match') + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +class DeleteUsers(AmCliCommand): + """A command for deleting one or more existing users.""" + def __init__(self, app, app_args, cmd_name=None): + super(DeleteUsers, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User deleted.' + + +class ListUsers(AmCliLister): + """A command for listing existing users.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListUsers, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [UUID, NAME, ENABLED, PASSWORDEXP] + self.default_sort = [NAME, 'asc'] + + +@password_policy_docstring +class ChangeUserPassword(AmCliCommand): + """A command for changing the current user password (i.e. own password). + The old and new passwords are prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(ChangeUserPassword, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users/ownpasswords' + #self.mandatory_positional = False + self.no_positional = True + self.arguments = [OLDPASSWORD, NEWPASSWORD] + self.message = 'Your password has been changed.' + self.auth_required = False + + def take_action(self, parsed_args): + try: + if parsed_args.opassword == '': + parsed_args.opassword = getpass.getpass(prompt='Old password: ') + if parsed_args.npassword == '': + npassword1 = getpass.getpass(prompt='New password: ') + npassword2 = getpass.getpass(prompt='New password again: ') + if npassword1 == npassword2: + parsed_args.npassword = npassword1 + else: + raise Exception('New passwords do not match') + parsed_args.username = os.environ['OS_USERNAME'] + self.arguments.append(USERNAME) + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +@password_policy_docstring +class ResetUserPassword(AmCliCommand): + """A command for user administrators for changing other user's password. + Own password cannot be changed with this command. + Note that user management admin role is required. + The new password is prompted if not given as parameter. + %PASSWORDPOLICY_DOCSTRING%""" + def __init__(self, app, app_args, cmd_name=None): + super(ResetUserPassword, self).__init__(app, app_args, cmd_name) + self.usebody = True + self.operation = 'post' + self.endpoint = 'users/passwords' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER, NEWPASSWORD] + self.message = 'Password has been reset for the user.' + + def take_action(self, parsed_args): + try: + if parsed_args.npassword == '': + npassword1 = getpass.getpass(prompt='New password: ') + npassword2 = getpass.getpass(prompt='New password again: ') + if npassword1 == npassword2: + parsed_args.npassword = npassword1 + else: + raise Exception('New passwords do not match') + result = self.send_receive(self.app, parsed_args) + if self.message: + self.app.stdout.write(ResetUserPassword.construct_message(self.message, result)) + except Exception as exp: + self.app.stderr.write('Failed with error %s\n' % str(exp)) + sys.exit(1) + + +class SetUserParameters(AmCliCommand): + """A command for setting user parameters.""" + def __init__(self, app, app_args, cmd_name=None): + super(SetUserParameters, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/parameters' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER, PROJECTID, EMAIL] + self.message = 'Parameter of the user is changed.' + + +class ShowUserDetails(AmCliShowOne): + """A command for displaying the details of a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowUserDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users/details' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES] + + +class ShowUserOwnDetails(AmCliShowOne): + """A command for displaying the details of a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowUserOwnDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'users/owndetails' + self.mandatory_positional = True + self.positional_count = 0 + self.columns = [DEFAULTPROJECTID, DOMAINID, EMAIL, ENABLED, UUID, LINKS, NAME, OPTIONS, PASSWORDEXP, ROLES] + + +class AddRoleForUser(AmCliCommand): + """A command for adding role to a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddRoleForUser, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/roles' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, ROLENAME] + self.message = 'Role has been added to the user.' + + +class RemoveRoleFromUser(AmCliCommand): + """A command for removing role from a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemoveRoleFromUser, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/roles' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, ROLENAME] + self.message = 'Role has been removed from the user.' + + +class LockUser(AmCliCommand): + """A command for locking an account.""" + def __init__(self, app, app_args, cmd_name=None): + super(LockUser, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/locks' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User has been locked.' + + +class UnlockUser(AmCliCommand): + """A command for enabling a locked account.""" + def __init__(self, app, app_args, cmd_name=None): + super(UnlockUser, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/locks' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'User has been enabled.' + + +class CreateNewRole(AmCliCommand): + """A command for creating a new role.""" + def __init__(self, app, app_args, cmd_name=None): + super(CreateNewRole, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME, ROLEDESC] + self.message = 'Role has been created.' + + +class ModifyRole(AmCliCommand): + """A command for modifying an existing role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ModifyRole, self).__init__(app, app_args, cmd_name) + self.operation = 'put' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME, ROLEDESC] + self.message = 'Role has been modified.' + + +class DeleteRole(AmCliCommand): + """A command for deleting one or more existing roles.""" + def __init__(self, app, app_args, cmd_name=None): + super(DeleteRole, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'roles' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.message = 'Role has been deleted.' + + +class ListRoles(AmCliLister): + """A command for listing existing roles. Openstack roles won't be listed.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListRoles, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [ROLENAME, ROLEDESC, ISSERVICE, ISCHROOT] + self.default_sort = [ROLENAME, 'asc'] + + +class ShowRoleDetails(AmCliLister): + """A command for displaying the details of a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ShowRoleDetails, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles/details' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.columns = [PERMISSIONNAME, PERMISSIONRES] + + +class ListUsersOfRole(AmCliLister): + """A command for listing the users of a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListUsersOfRole, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'roles/users' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [ROLENAME] + self.columns = [ROLENAME, USERS] + + +class AddPermissionToRole(AmCliCommand): + """A command for adding a new permission to a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddPermissionToRole, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'roles/permissions' + self.mandatory_positional = True + self.positional_count = 3 + self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP] + self.message = 'New permission added to role.' + + +class RemovePermissionFromRole(AmCliCommand): + """A command for removing a permission from a role.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemovePermissionFromRole, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'roles/permissions' + self.mandatory_positional = True + self.positional_count = 3 + self.arguments = [ROLENAME, RESOURCEPATH, RESOURCEOP] + self.message = 'Permission deleted from role.' + + +class ListPermissions(AmCliLister): + """A command for listing all the permissions and endpoints.""" + def __init__(self, app, app_args, cmd_name=None): + super(ListPermissions, self).__init__(app, app_args, cmd_name) + self.operation = 'get' + self.endpoint = 'permissions' + self.positional_count = 0 + self.arguments = [SORT] + self.columns = [PERMISSIONNAME, PERMISSIONRES] + self.default_sort = [PERMISSIONNAME, 'asc'] + + +class AddKey(AmCliCommand): + """A command for adding a public ssh key to a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(AddKey, self).__init__(app, app_args, cmd_name) + self.operation = 'post' + self.endpoint = 'users/keys' + self.mandatory_positional = True + self.positional_count = 2 + self.arguments = [USER, PUBSSHKEY] + self.message = 'Key added to the user.' + + +class RemoveKey(AmCliCommand): + """A command for removing a public ssh key from a user.""" + def __init__(self, app, app_args, cmd_name=None): + super(RemoveKey, self).__init__(app, app_args, cmd_name) + self.operation = 'delete' + self.endpoint = 'users/keys' + self.mandatory_positional = True + self.positional_count = 1 + self.arguments = [USER] + self.message = 'Key removed from the user.'