From f0cefcc77d593198b6e295e92a8bf05fedc8c8ff Mon Sep 17 00:00:00 2001
From: Bhuvan Arumugam <bhuvan@apache.org>
Date: Sun, 8 Jul 2012 16:06:32 -0700
Subject: [PATCH] Keyring support for openstackclient.

Bug: 1030440

If password is defined in keyring, use it; otherwise, prompt for the
password. Keying is configured using command line switch,
--os-use-keyring or env(OS_USE_KEYRING).

* openstackclient/common/openstackkeyring.py
  The abstract class for keyring, specifically for openstack. The
  class is used to store encrypted password in keyring, without
  prompting for keyring password. The encrypted password is
  stored in ~/.openstack-keyring.cfg file.

* openstack-common.py
  Update openstackkeyring library from openstack.common.

* openstackclient/shell.py
  OpenStackClient.build_option_parser(): New boolean argument,
   --os-use-keyring, default to env(OS_USE_KEYRING).
  OpenStackClient.authenticate_user(): Get password from keyring,
  if it is defined; otherwise, prompt for the password. If user
  enter a password and keyring is enabled, store it in keyring.
  OpenStackClient.init_keyring_backend(): New method to define
  openstack backend for keyring.
  OpenStackClient.get_password_from_keyring(): New method to
  get password from keyring.
  OpenStackClient.set_password_in_keyring(): New method go set
  password in keyring.

* toos/pip-requires
  Define keyring and pycrypto as one of dependent.

Change-Id: I36d3a63054658c0ef0553d68b38fefbc236930ef
---
 openstack-common.conf                      |  2 +-
 openstackclient/common/openstackkeyring.py | 65 ++++++++++++++++++++++
 openstackclient/shell.py                   | 44 +++++++++++++++
 tools/pip-requires                         |  2 +
 4 files changed, 112 insertions(+), 1 deletion(-)
 create mode 100644 openstackclient/common/openstackkeyring.py

diff --git a/openstack-common.conf b/openstack-common.conf
index 08e091b17a..6b8b86a0c7 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -1,7 +1,7 @@
 [DEFAULT]
 
 # The list of modules to copy from openstack-common
-modules=setup
+modules=setup,openstackkeyring
 
 # The base module to hold the copy of openstack.common
 base=openstackclient
diff --git a/openstackclient/common/openstackkeyring.py b/openstackclient/common/openstackkeyring.py
new file mode 100644
index 0000000000..3a5ce27f8c
--- /dev/null
+++ b/openstackclient/common/openstackkeyring.py
@@ -0,0 +1,65 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved
+#
+#    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.
+#
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+"""
+Keyring backend for Openstack, to store encrypted password in a file.
+"""
+
+from Crypto.Cipher import AES
+
+import crypt
+import keyring
+import os
+
+KEYRING_FILE = os.path.join(os.path.expanduser('~'), '.openstack-keyring.cfg')
+
+
+class OpenstackKeyring(keyring.backend.BasicFileKeyring):
+    """ Openstack Keyring to store encrypted password """
+
+    filename = KEYRING_FILE
+
+    def supported(self):
+        """ applicable for all platforms, but not recommend """
+        pass
+
+    def _init_crypter(self):
+        """ initialize the crypter using the class name """
+        block_size = 32
+        padding = '0'
+
+        # init the cipher with the class name, upto block_size
+        password = __name__[block_size:]
+        password = password + (block_size - len(password) % \
+                              block_size) * padding
+        return AES.new(password, AES.MODE_CFB)
+
+    def encrypt(self, password):
+        """ encrypt the given password """
+        crypter = self._init_crypter()
+        return crypter.encrypt(password)
+
+    def decrypt(self, password_encrypted):
+        """ decrypt the given password """
+        crypter = self._init_crypter()
+        return crypter.decrypt(password_encrypted)
+
+
+def os_keyring():
+    """ initialize the openstack keyring """
+    return keyring.core.load_keyring(None,
+             'openstackclient.common.openstackkeyring.OpenstackKeyring')
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 3d0adf9937..531ac258d6 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -29,10 +29,12 @@ from cliff.commandmanager import CommandManager
 
 from openstackclient.common import clientmanager
 from openstackclient.common import exceptions as exc
+from openstackclient.common import openstackkeyring
 from openstackclient.common import utils
 
 
 VERSION = '0.1'
+KEYRING_SERVICE = 'openstack'
 
 
 def env(*vars, **kwargs):
@@ -123,6 +125,18 @@ class OpenStackShell(App):
             default=env('OS_URL'),
             help='Defaults to env[OS_URL]')
 
+        env_os_keyring = env('OS_USE_KEYRING', default=False)
+        if type(env_os_keyring) == str:
+            if env_os_keyring.lower() in ['true', '1']:
+                env_os_keyring = True
+            else:
+                env_os_keyring = False
+        parser.add_argument('--os-use-keyring',
+                            default=env_os_keyring,
+                            action='store_true',
+                            help='Use keyring to store password, '
+                                 'default=False (Env: OS_USE_KEYRING)')
+
         return parser
 
     def authenticate_user(self):
@@ -149,12 +163,14 @@ class OpenStackShell(App):
                     "You must provide a username via"
                     " either --os-username or env[OS_USERNAME]")
 
+            self.get_password_from_keyring()
             if not self.options.os_password:
                 # No password, if we've got a tty, try prompting for it
                 if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
                     # Check for Ctl-D
                     try:
                         self.options.os_password = getpass.getpass()
+                        self.set_password_in_keyring()
                     except EOFError:
                         pass
                 # No password because we did't have a tty or the
@@ -188,6 +204,34 @@ class OpenStackShell(App):
             )
         return
 
+    def init_keyring_backend(self):
+        """Initialize openstack backend to use for keyring"""
+        return openstackkeyring.os_keyring()
+
+    def get_password_from_keyring(self):
+        """Get password from keyring, if it's set"""
+        if self.options.os_use_keyring:
+            service = KEYRING_SERVICE
+            backend = self.init_keyring_backend()
+            if not self.options.os_password:
+                password = backend.get_password(service,
+                                                self.options.os_username)
+                self.options.os_password = password
+
+    def set_password_in_keyring(self):
+        """Set password in keyring for this user"""
+        if self.options.os_use_keyring:
+            service = KEYRING_SERVICE
+            backend = self.init_keyring_backend()
+            if self.options.os_password:
+                password = backend.get_password(service,
+                                                self.options.os_username)
+                # either password is not set in keyring, or it is different
+                if password != self.options.os_password:
+                    backend.set_password(service,
+                                         self.options.os_username,
+                                         self.options.os_password)
+
     def initialize_app(self, argv):
         """Global app init bits:
 
diff --git a/tools/pip-requires b/tools/pip-requires
index efcaf7fe01..d4ad68b62a 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -1,7 +1,9 @@
 cliff
 argparse
 httplib2
+keyring
 prettytable
+pycrypto
 python-keystoneclient>=0.1,<0.2
 python-novaclient>=2,<3
 simplejson