#!/usr/bin/python -tt # # Copyright 2015 Red Hat, Inc. # License: GPLv2+ # Author: Pierre-Yves Chibon # Author: Toshio Kuratomi from __future__ import print_function __version_info__ = ((0, 1),) import os import grp import sys import argparse from getpass import getpass, getuser from configobj import ConfigObj, flatten_errors from fedora.client import AccountSystem, AuthError from validate import Validator retries = 0 MAX_RETRIES = 3 def parse_commandline(args): parser = argparse.ArgumentParser( description='Generate an ssh authorized_keys file using fas') parser.add_argument('users', metavar='USERS', type=str, nargs='+', help='FAS usernames and FAS group names preceeded by "@".' ' For example: toshio skvidal @gitfas') parser.add_argument('--with-fas-conf', dest='fas_conf', action='store_true', default=True, help='Look for credentials in /etc/fas.conf. On hosts that run' ' fasClient to set up users and groups, fas.conf will have' ' a username and password that will work for us. This can only be' ' read if the user is root. Otherwise a different method of' ' getting credentials will be used.') parser.add_argument('--without-fas-conf', dest='fas_conf', action='store_false', default=True, help='Look for credentials in /etc/fas.conf. On hosts that run' ' fasClient to set up users and groups, fas.conf will have' ' a username and password that will work for us. This can only be' ' read if the user is root. Otherwise a different method of' ' getting credentials will be used.') # The following can be set in the config files so the default value is # taken from there parser.add_argument('--limit-from', '-l', action='append', default=None, help='IP addresses that limit where the keys can be used to' ' ssh from') parser.add_argument('--with-fas-groups', dest='fas_groups', action='store_true', default=None, help='Query fas for members of groups. The default is to use the' ' groups information on the local machine. Querying fas can be' ' used on machines that do not get their group information from' ' fas or if you need group information that has been updated' ' recently but it is slower.') parser.add_argument('--without-fas-groups', dest='fas_groups', action='store_false', default=None, help='Query fas for members of groups. The default is to use the' ' groups information on the local machine. Querying fas can be' ' used on machines that do not get their group information from' ' fas or if you need group information that has been updated' ' recently but it is slower.') parser.add_argument('--username', '-u', type=str, action='store', default=None, help='Use this username when contacting fas. This will not be' ' used if a username and password can be read from /etc/fas.conf') parsed = parser.parse_args(args) args_dict = {} args_dict['fas_conf'] = parsed.fas_conf if parsed.limit_from is not None: args_dict['limit_from'] = parsed.limit_from if parsed.fas_groups is not None: args_dict['fas_groups'] = parsed.fas_groups if parsed.username is not None: args_dict['username'] = parsed.username users = set() groups = set() for entry in parsed.users: if entry.startswith('@'): groups.add(entry[1:]) elif entry == sys.argv[0]: # argparse hasn't already subtracted the program name here.... continue else: users.add(entry) args_dict['users'] = users args_dict['groups'] = groups return args_dict def read_config_files(cfg_files): config_spec = ( 'limit_from = ip_addr_list(default=list())', 'fas_groups = boolean(default=False)', 'username = string(default=%s)' % getuser(), ) options = ConfigObj(configspec=config_spec) validator = Validator() for cfg_file in cfg_files: cfg_file = os.path.abspath(os.path.expanduser(cfg_file)) cfg = ConfigObj(cfg_file, configspec=config_spec) options.merge(cfg) results = options.validate(validator) if results != True: for (section_list, key, unused_) in flatten_errors(options, results): if key is not None: print('The "%s" key in the section "%s" failed validation' % ( key, ', '.join(section_list))) else: print('The following section was missing:%s ' % ', '.join( section_list)) sys.exit(1) return options def read_fas_conf(): '''Read username and password from /etc/fas.conf This function will trhow an exception if it can't read the information. otherwise it returns username, password ''' cfgfile = open('/etc/fas.conf', 'r') # Turn comment markers ";" into "#" change_comment = lambda line: (line.startswith(';') and line.replace(';', '#', 1) ) or line lines = [change_comment(l) for l in cfgfile] cfg = ConfigObj(lines) return cfg['global']['login'], cfg['global']['password'] def members_of_group(group, use_fas=False): if use_fas: members = retry_fas(fas.group_members, group) members = [m.username for m in members] else: group_info = grp.getgrnam(group) members = group_info[3] return set(members) def retry_fas(function, *args, **kwargs): global retries # Try the function at least once while True: try: return function(*args, **kwargs) except AuthError: retries += 1 password = getpass('FAS Password for %s:' % function.__self__.username) function.__self__.password = password if retries >= MAX_RETRIES: raise if __name__ == '__main__': args = parse_commandline(sys.argv) conf = read_config_files(['/etc/fas-auth-keys.conf', '~/.fedora/fas-auth-keys.conf']) # Merge args and conf. Note that this requires that we've already parsed # our args so that they're in a dict and won't overwrite pieces of conf if # they weren't specified on the commandline conf.merge(args) # Merge in fas.conf if conf['fas_conf']: try: username, password = read_fas_conf() except: # We don't care -- this is a nicety pass else: conf['username'] = username conf['password'] = password fas = AccountSystem(username=conf['username'], password=conf['password']) from_string = '' if conf['limit_from']: from_string = 'from="%s" ' % ','.join(conf['limit_from']) for group in conf['groups']: conf['users'].update(members_of_group(group, use_fas=conf['fas_groups'])) for user in sorted(set(conf['users'])): print(user)