summaryrefslogtreecommitdiffstats
path: root/scripts/users-from-fas
blob: 9d1b309374b5b2bce90ae278078954a2a7d0f7e9 (plain)
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/python -tt
#
# Copyright 2015 Red Hat, Inc.
# License: GPLv2+
# Author: Pierre-Yves Chibon <pingou@pingoured.fr>
# Author: Toshio Kuratomi <toshio@fedoraproject.org>

__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.im_self.username)
            function.im_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