238 lines
8.4 KiB
Python
238 lines
8.4 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2013 Google 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.
|
|
|
|
"""A git credential helper that provides Google git repository passwords.
|
|
|
|
Reads a session from stdin that looks a lot like:
|
|
protocol=https
|
|
host=code.google.com
|
|
And writes out a session to stdout that looks a lot like:
|
|
username=me
|
|
password=secret
|
|
|
|
Errors will be reported on stderr.
|
|
|
|
Note that spaces may be part of key names so, for example, "protocol" must not
|
|
be proceeded by leading spaces.
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
|
|
from googlecloudsdk.api_lib.auth import exceptions as auth_exceptions
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions as c_exc
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.credentials import creds as c_creds
|
|
from googlecloudsdk.core.credentials import exceptions as creds_exceptions
|
|
from googlecloudsdk.core.credentials import store as c_store
|
|
from googlecloudsdk.core.util import files
|
|
from googlecloudsdk.core.util import platforms
|
|
|
|
from oauth2client import client
|
|
import six
|
|
|
|
|
|
_KEYVAL_RE = re.compile(r'(.+)=(.*)')
|
|
_BLANK_LINE_RE = re.compile(r'^ *$')
|
|
|
|
|
|
@base.Hidden
|
|
@base.DefaultUniverseOnly
|
|
class GitHelper(base.Command):
|
|
"""A git credential helper to provide access to Google git repositories."""
|
|
|
|
GET = 'get'
|
|
STORE = 'store'
|
|
METHODS = [GET, STORE]
|
|
|
|
GOOGLESOURCE = 'googlesource.com'
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
parser.add_argument('method',
|
|
help='The git credential helper method.')
|
|
parser.add_argument('--ignore-unknown',
|
|
action='store_true',
|
|
help=('Produce no output and exit with 0 when given '
|
|
'an unknown method (e.g. store) or host.'))
|
|
|
|
@c_exc.RaiseErrorInsteadOf(auth_exceptions.AuthenticationError, client.Error)
|
|
def Run(self, args):
|
|
"""Run the helper command."""
|
|
|
|
# Disable self signed jwt for this command.
|
|
properties.VALUES.auth.service_account_use_self_signed_jwt.Set(False)
|
|
|
|
if args.method not in GitHelper.METHODS:
|
|
if args.ignore_unknown:
|
|
return
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Unexpected method [{meth}]. One of [{methods}] expected.'
|
|
.format(meth=args.method, methods=', '.join(GitHelper.METHODS)))
|
|
|
|
info = self._ParseInput()
|
|
credentialed_domains = [
|
|
'source.developers.google.com',
|
|
GitHelper.GOOGLESOURCE, # Requires a different username value.
|
|
]
|
|
credentialed_domains_suffix = [
|
|
'.sourcemanager.dev',
|
|
'.blueoryx.dev',
|
|
'.developerconnect.dev',
|
|
'.'+GitHelper.GOOGLESOURCE,
|
|
]
|
|
extra = properties.VALUES.core.credentialed_hosted_repo_domains.Get()
|
|
if extra:
|
|
credentialed_domains.extend(extra.split(','))
|
|
host = info.get('host')
|
|
|
|
def _ValidateHost(host):
|
|
if host in credentialed_domains:
|
|
return True
|
|
for suffix in credentialed_domains_suffix:
|
|
if host.endswith(suffix):
|
|
return True
|
|
return False
|
|
|
|
if not _ValidateHost(host):
|
|
if not args.ignore_unknown:
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Unknown host [{host}].'.format(host=host))
|
|
return
|
|
|
|
if args.method == GitHelper.GET:
|
|
account = properties.VALUES.core.account.Get()
|
|
try:
|
|
cred = c_store.Load(account, use_google_auth=True)
|
|
c_store.Refresh(cred)
|
|
except creds_exceptions.Error as e:
|
|
sys.stderr.write(textwrap.dedent("""\
|
|
ERROR: {error}
|
|
Run 'gcloud auth login' to log in.
|
|
""".format(error=six.text_type(e))))
|
|
return
|
|
|
|
self._CheckNetrc()
|
|
|
|
# For googlesource.com, any username beginning with "git-" is accepted
|
|
# and the identity of the user is extracted from the token server-side.
|
|
if (host == GitHelper.GOOGLESOURCE
|
|
or host.endswith('.'+GitHelper.GOOGLESOURCE)):
|
|
sent_account = 'git-account'
|
|
else:
|
|
sent_account = account
|
|
|
|
if c_creds.IsOauth2ClientCredentials(cred):
|
|
access_token = cred.access_token
|
|
else:
|
|
access_token = cred.token
|
|
|
|
sys.stdout.write(
|
|
textwrap.dedent("""\
|
|
username={username}
|
|
password={password}
|
|
""").format(username=sent_account, password=access_token))
|
|
elif args.method == GitHelper.STORE:
|
|
# On OSX, there is an additional credential helper that gets called before
|
|
# ours does. When we return a token, it gets cached there. Git continues
|
|
# to get it from there first until it expires. That command then fails,
|
|
# and the token is deleted, but it does not retry the operation. The next
|
|
# command gets a new token from us and it starts working again, for an
|
|
# hour. This erases our credential from the other cache whenever 'store'
|
|
# is called on us. Because they are called first, the token will already
|
|
# be stored there, and so we can successfully erase it to prevent caching.
|
|
if (platforms.OperatingSystem.Current() ==
|
|
platforms.OperatingSystem.MACOSX):
|
|
log.debug('Clearing OSX credential cache.')
|
|
try:
|
|
input_string = 'protocol={protocol}\nhost={host}\n\n'.format(
|
|
protocol=info.get('protocol'), host=info.get('host'))
|
|
log.debug('Calling erase with input:\n%s', input_string)
|
|
p = subprocess.Popen(['git-credential-osxkeychain', 'erase'],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
(out, err) = p.communicate(input_string)
|
|
if p.returncode:
|
|
log.debug(
|
|
'Failed to clear OSX keychain:\nstdout: {%s}\nstderr: {%s}',
|
|
out, err)
|
|
# pylint:disable=broad-except, This can fail and should only be done as
|
|
# best effort.
|
|
except Exception as e:
|
|
log.debug('Failed to clear OSX keychain', exc_info=True)
|
|
|
|
def _ParseInput(self):
|
|
"""Parse the fields from stdin.
|
|
|
|
Returns:
|
|
{str: str}, The parsed parameters given on stdin.
|
|
"""
|
|
info = {}
|
|
for line in sys.stdin:
|
|
if _BLANK_LINE_RE.match(line):
|
|
continue
|
|
match = _KEYVAL_RE.match(line)
|
|
if not match:
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Invalid input line format: [{format}].'
|
|
.format(format=line.rstrip('\n')))
|
|
key, val = match.groups()
|
|
info[key] = val.strip()
|
|
|
|
if 'protocol' not in info:
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Required key "protocol" missing.')
|
|
|
|
if 'host' not in info:
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Required key "host" missing.')
|
|
|
|
if info.get('protocol') != 'https':
|
|
raise auth_exceptions.GitCredentialHelperError(
|
|
'Invalid protocol [{p}]. "https" expected.'
|
|
.format(p=info.get('protocol')))
|
|
return info
|
|
|
|
def _CheckNetrc(self):
|
|
"""Warn on stderr if ~/.netrc contains redundant credentials."""
|
|
|
|
def Check(p):
|
|
"""Warn about other credential helpers that will be ignored."""
|
|
if not os.path.exists(p):
|
|
return
|
|
try:
|
|
data = files.ReadFileContents(p)
|
|
if 'source.developers.google.com' in data:
|
|
sys.stderr.write(textwrap.dedent("""\
|
|
You have credentials for your Google repository in [{path}]. This repository's
|
|
git credential helper is set correctly, so the credentials in [{path}] will not
|
|
be used, but you may want to remove them to avoid confusion.
|
|
""".format(path=p)))
|
|
# pylint:disable=broad-except, If something went wrong, forget about it.
|
|
except Exception:
|
|
pass
|
|
Check(files.ExpandHomeDir(os.path.join('~', '.netrc')))
|
|
Check(files.ExpandHomeDir(os.path.join('~', '_netrc')))
|