Added seed code for access-management.
[ta/access-management.git] / src / access_management / cli / cli.py
diff --git a/src/access_management/cli/cli.py b/src/access_management/cli/cli.py
new file mode 100644 (file)
index 0000000..15b27d9
--- /dev/null
@@ -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 <key>[:<asc|desc>]. 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.'