1 # Copyright (c) 2009 Google Inc. All rights reserved. 2 # Copyright (c) 2009 Apple Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 # 30 # Python module for reading stored web credentials from the OS. 31 32 import logging 33 import os 34 import platform 35 import re 36 37 from webkitpy.common.checkout.scm import Git 38 from webkitpy.common.system.executive import Executive, ScriptError 39 from webkitpy.common.system.user import User 40 41 try: 42 # Use keyring, a cross platform keyring interface, as a fallback: 43 # http://pypi.python.org/pypi/keyring 44 import keyring 45 except ImportError: 46 keyring = None 47 48 _log = logging.getLogger(__name__) 49 50 51 class Credentials(object): 52 _environ_prefix = "webkit_bugzilla_" 53 54 def __init__(self, host, git_prefix=None, executive=None, cwd=os.getcwd(), 55 keyring=keyring): 56 self.host = host 57 self.git_prefix = "%s." % git_prefix if git_prefix else "" 58 self.executive = executive or Executive() 59 self.cwd = cwd 60 self._keyring = keyring 61 62 def _credentials_from_git(self): 63 try: 64 if not Git.in_working_directory(self.cwd): 65 return (None, None) 66 return (Git.read_git_config(self.git_prefix + "username"), 67 Git.read_git_config(self.git_prefix + "password")) 68 except OSError, e: 69 # Catch and ignore OSError exceptions such as "no such file 70 # or directory" (OSError errno 2), which imply that the Git 71 # command cannot be found/is not installed. 72 pass 73 return (None, None) 74 75 def _keychain_value_with_label(self, label, source_text): 76 match = re.search("%s\"(?P<value>.+)\"" % label, 77 source_text, 78 re.MULTILINE) 79 if match: 80 return match.group('value') 81 82 def _is_mac_os_x(self): 83 return platform.mac_ver()[0] 84 85 def _parse_security_tool_output(self, security_output): 86 username = self._keychain_value_with_label("^\s*\"acct\"<blob>=", 87 security_output) 88 password = self._keychain_value_with_label("^password: ", 89 security_output) 90 return [username, password] 91 92 def _run_security_tool(self, username=None): 93 security_command = [ 94 "/usr/bin/security", 95 "find-internet-password", 96 "-g", 97 "-s", 98 self.host, 99 ] 100 if username: 101 security_command += ["-a", username] 102 103 _log.info("Reading Keychain for %s account and password. " 104 "Click \"Allow\" to continue..." % self.host) 105 try: 106 return self.executive.run_command(security_command) 107 except ScriptError: 108 # Failed to either find a keychain entry or somekind of OS-related 109 # error occured (for instance, couldn't find the /usr/sbin/security 110 # command). 111 _log.error("Could not find a keychain entry for %s." % self.host) 112 return None 113 114 def _credentials_from_keychain(self, username=None): 115 if not self._is_mac_os_x(): 116 return [username, None] 117 118 security_output = self._run_security_tool(username) 119 if security_output: 120 return self._parse_security_tool_output(security_output) 121 else: 122 return [None, None] 123 124 def _read_environ(self, key): 125 environ_key = self._environ_prefix + key 126 return os.environ.get(environ_key.upper()) 127 128 def _credentials_from_environment(self): 129 return (self._read_environ("username"), self._read_environ("password")) 130 131 def _offer_to_store_credentials_in_keyring(self, username, password): 132 if not self._keyring: 133 return 134 if not User().confirm("Store password in system keyring?", User.DEFAULT_NO): 135 return 136 try: 137 self._keyring.set_password(self.host, username, password) 138 except: 139 pass 140 141 def read_credentials(self, user=User): 142 username, password = self._credentials_from_environment() 143 # FIXME: We don't currently support pulling the username from one 144 # source and the password from a separate source. 145 if not username or not password: 146 username, password = self._credentials_from_git() 147 if not username or not password: 148 username, password = self._credentials_from_keychain(username) 149 150 if not username: 151 username = user.prompt("%s login: " % self.host) 152 153 if username and not password and self._keyring: 154 try: 155 password = self._keyring.get_password(self.host, username) 156 except: 157 pass 158 159 if not password: 160 password = user.prompt_password("%s password for %s: " % (self.host, username)) 161 self._offer_to_store_credentials_in_keyring(username, password) 162 163 return (username, password) 164