/usr/bin/pass-git-helper is in pass-git-helper 0.4-1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | #!/usr/bin/python3
import argparse
import configparser
import fnmatch
import logging
import os
import os.path
import subprocess
import sys
import xdg.BaseDirectory
LOGGER = logging.getLogger()
CONFIG_FILE_NAME = 'git-pass-mapping.ini'
DEFAULT_CONFIG_FILE = os.path.join(
xdg.BaseDirectory.save_config_path('pass-git-helper'),
CONFIG_FILE_NAME)
def parse_arguments():
parser = argparse.ArgumentParser(
description='Git credential helper using pass as the data source.',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument(
'-m', '--mapping',
type=argparse.FileType('r'),
metavar='MAPPING_FILE',
default=None,
help='A mapping file to be used, specifying how hosts '
'map to pass entries. Overrides the default mapping files from '
'XDG config locations, usually: {}'.format(DEFAULT_CONFIG_FILE))
parser.add_argument(
'-l', '--logging',
action='store_true',
default=False,
help='Print debug messages on stderr. '
'Might include sensitive information')
parser.add_argument(
'action',
type=str,
metavar='ACTION',
help='Action to preform as specified in the git credential API')
args = parser.parse_args()
return args
def parse_mapping(mapping_file):
LOGGER.debug('Parsing mapping file. Command line: %s', mapping_file)
def parse(mapping_file):
config = configparser.ConfigParser()
config.read_file(mapping_file)
return config
# give precedence to the user-specified file
if mapping_file is not None:
LOGGER.debug('Parsing command line mapping file')
return parse(mapping_file)
# fall back on XDG config location
xdg_config_dir = xdg.BaseDirectory.load_first_config('pass-git-helper')
if xdg_config_dir is None:
raise RuntimeError(
'No mapping configured so far at any XDG config location. '
'Please create {}'.format(DEFAULT_CONFIG_FILE))
mapping_file = os.path.join(xdg_config_dir, CONFIG_FILE_NAME)
LOGGER.debug('Parsing mapping file %s', mapping_file)
with open(mapping_file, 'r') as file_handle:
return parse(file_handle)
def parse_request():
in_lines = sys.stdin.readlines()
LOGGER.debug('Received request "%s"', in_lines)
request = {}
for line in in_lines:
# skip empty lines to be a bit resilient against protocol errors
if not line.strip():
continue
parts = line.split('=', 1)
assert len(parts) == 2
request[parts[0].strip()] = parts[1].strip()
return request
def get_password(request, mapping):
LOGGER.debug('Received request "%s"', request)
if 'host' not in request:
LOGGER.error('host= entry missing in request. '
'Cannot query without a host')
return
host = request['host']
if 'path' in request:
host = os.path.join(host, request['path'])
def decode_skip(line, skip):
return line.decode('utf-8')[skip:]
LOGGER.debug('Iterating mapping to match against host "%s"', host)
for section in mapping.sections():
if fnmatch.fnmatch(host, section):
LOGGER.debug('Section "%s" matches requested host "%s"',
section, host)
# TODO handle exceptions
pass_target = mapping.get(section, 'target').replace("${host}", host)
skip_password_chars = mapping.getint(
section, 'skip_password', fallback=0)
skip_username_chars = mapping.getint(
section, 'skip_username', fallback=0)
LOGGER.debug('Requesting entry "%s" from pass', pass_target)
output = subprocess.check_output(['pass', 'show', pass_target])
lines = output.splitlines()
if len(lines) >= 1:
print('password={}'.format(
decode_skip(lines[0], skip_password_chars)))
if 'username' not in request and len(lines) >= 2:
print('username={}'.format(
decode_skip(lines[1], skip_username_chars)))
return
LOGGER.warning('No mapping matched')
sys.exit(1)
def handle_skip():
if 'PASS_GIT_HELPER_SKIP' in os.environ:
LOGGER.info(
'Skipping processing as requested via environment variable')
sys.exit(1)
def main():
args = parse_arguments()
if args.logging:
logging.basicConfig(level=logging.DEBUG)
handle_skip()
action = args.action
request = parse_request()
LOGGER.debug('Received action %s with request:\n%s',
action, request)
try:
mapping = parse_mapping(args.mapping)
except Exception as error:
LOGGER.critical('Unable to parse mapping file', exc_info=True)
print('Unable to parse mapping file: {}'.format(error),
file=sys.stderr)
sys.exit(1)
if action == 'get':
get_password(request, mapping)
else:
LOGGER.info('Action %s is currently not supported', action)
sys.exit(1)
if __name__ == '__main__':
main()
|