#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright © 2007-2008 Red Hat, Inc. All rights reserved. # # This copyrighted material is made available to anyone wishing to use, modify, # copy, or redistribute it subject to the terms and conditions of the GNU # General Public License v.2. This program is distributed in the hope that it # will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the # implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. You should have # received a copy of the GNU General Public License along with this program; # if not, write to the Free Software Foundation, Inc., 51 Franklin Street, # Fifth Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are # incorporated in the source code or documentation are not subject to the GNU # General Public License and may only be used or replicated with the express # permission of Red Hat, Inc. # # Red Hat Author(s): Mike McGrath # # TODO: put tmp files in a 700 tmp dir import sys import logging import syslog import os import tempfile import codecs import datetime import time from fedora.tg.client import BaseClient, AuthError, ServerError from optparse import OptionParser from shutil import move, rmtree, copytree from rhpl.translate import _ import ConfigParser parser = OptionParser() parser.add_option('-i', '--install', dest = 'install', default = False, action = 'store_true', help = _('Download and sync most recent content')) parser.add_option('-c', '--config', dest = 'CONFIG_FILE', default = '/etc/fas.conf', metavar = 'CONFIG_FILE', help = _('Specify config file (default "%default")')) parser.add_option('--nogroup', dest = 'no_group', default = False, action = 'store_true', help = _('Do not sync group information')) parser.add_option('--nopasswd', dest = 'no_passwd', default = False, action = 'store_true', help = _('Do not sync passwd information')) parser.add_option('--noshadow', dest = 'no_shadow', default = False, action = 'store_true', help = _('Do not sync shadow information')) parser.add_option('--nohome', dest = 'no_home_dirs', default = False, action = 'store_true', help = _('Do not create home dirs')) parser.add_option('--nossh', dest = 'no_ssh_keys', default = False, action = 'store_true', help = _('Do not create ssh keys')) parser.add_option('-s', '--server', dest = 'FAS_URL', default = None, metavar = 'FAS_URL', help = _('Specify URL of fas server.')) parser.add_option('-p', '--prefix', dest = 'prefix', default = None, metavar = 'prefix', help = _('Specify install prefix. Useful for testing')) parser.add_option('-e', '--enable', dest = 'enable', default = False, action = 'store_true', help = _('Enable FAS synced shell accounts')) parser.add_option('-d', '--disable', dest = 'disable', default = False, action = 'store_true', help = _('Disable FAS synced shell accounts')) parser.add_option('-a', '--aliases', dest = 'aliases', default = False, action = 'store_true', help = _('Sync mail aliases')) (opts, args) = parser.parse_args() log = logging.getLogger('fas') try: config = ConfigParser.ConfigParser() if os.path.exists(opts.CONFIG_FILE): config.read(opts.CONFIG_FILE) elif os.path.exists('fas.conf'): config.read('fas.conf') print >> sys.stderr, "Could not open %s, defaulting to ./fas.conf" % opts.CONFIG_FILE else: print >> sys.stderr, "Could not open %s." % opts.CONFIG_FILE sys.exit(5) except ConfigParser.MissingSectionHeaderError, e: print >> sys.stderr, "Config file does not have proper formatting - %s" % e sys.exit(6) FAS_URL = config.get('global', 'url').strip('"') if opts.prefix: prefix = opts.prefix else: prefix = config.get('global', 'prefix').strip('"') def _chown(arg, dir_name, files): os.chown(dir_name, arg[0], arg[1]) for file in files: os.chown(os.path.join(dir_name, file), arg[0], arg[1]) class MakeShellAccounts(BaseClient): temp = None groups = None people = None memberships = None emails = None group_mapping = {} valid_groups = {} usernames = {} def mk_tempdir(self): self.temp = tempfile.mkdtemp('-tmp', 'fas-', os.path.join(prefix + config.get('global', 'temp').strip('"'))) def rm_tempdir(self): rmtree(self.temp) def valid_groups(self): ''' Create a dict of valid groups, including that of group_type ''' if not self.groups: self.group_list() valid_groups = {'groups':[], 'restricted_groups':[], 'ssh_restricted_groups': []} for restriction in valid_groups: for group in config.get('host', restriction).strip('"').split(','): if group == '': continue if group.startswith('@'): for grp in self.groups: if grp['group_type'] == group[1:]: valid_groups[restriction].append(grp['name']) else: valid_groups[restriction].append(group) self.valid_groups = valid_groups def valid_group(self, name, restriction=None): ''' Determine if group is valid on the system ''' if restriction: return name in self.valid_groups[restriction] else: for restrict_key in self.valid_groups: if name in self.valid_groups[restrict_key]: return True return False def valid_user(self, username): ''' Is the user valid on this system ''' if not self.valid_groups: self.valid_groups() if not self.group_mapping: self.get_group_mapping() try: for restriction in self.valid_groups: for group in self.valid_groups[restriction]: if username in self.group_mapping[group]: return True except KeyError: return False return False def ssh_key(self, person): ''' determine what ssh key a user should have ''' for group in self.valid_groups['groups']: try: if person['username'] in self.group_mapping[group]: return person['ssh_key'] except KeyError: print >> sys.stderr, '%s could not be found in fas but was in your config under "groups"!' % group continue for group in self.valid_groups['restricted_groups']: try: if person['username'] in self.group_mapping[group]: return person['ssh_key'] except KeyError: print >> sys.stderr, '%s could not be found in fas but was in your config under "restricted_groups"!' % group continue for group in self.valid_groups['ssh_restricted_groups']: try: if person['username'] in self.group_mapping[group]: command = config.get('users', 'ssh_restricted_app').strip('"') options = config.get('users', 'ssh_key_options').strip('"') key = 'command="%s",%s %s' % (command, options, person['ssh_key']) return key except TypeError: print >> sys.stderr, '%s could not be found in fas but was in your config under "ssh_restricted_groups"!' % group continue return 'INVALID\n' def shell(self, username): ''' Determine what shell username should have ''' for group in self.valid_groups['groups']: try: if username in self.group_mapping[group]: return config.get('users', 'shell').strip('"') except KeyError: print >> sys.stderr, '%s could not be found in fas but was in your config under "groups"!' % group continue for group in self.valid_groups['restricted_groups']: try: if username in self.group_mapping[group]: return config.get('users', 'restricted_shell').strip('"') except KeyError: print >> sys.stderr, '%s could not be found in fas but was in your config under "restricted_groups"!' % group continue for group in self.valid_groups['ssh_restricted_groups']: try: if username in self.group_mapping[group]: return config.get('users', 'ssh_restricted_shell').strip('"') except KeyError: print >> sys.stderr, '%s could not be found in fas but was in your config under "ssh_restricted_groups"!' % group continue print >> sys.stderr, 'Could not determine shell for %s. Defaulting to /sbin/nologin' % username return '/sbin/nologin' def install_aliases_txt(self): move(self.temp + '/aliases', prefix + '/etc/aliases') def passwd_text(self, people=None): i = 0 passwd_file = codecs.open(self.temp + '/passwd.txt', mode='w', encoding='utf-8') shadow_file = codecs.open(self.temp + '/shadow.txt', mode='w', encoding='utf-8') os.chmod(self.temp + '/shadow.txt', 00400) if not self.people: self.people_list() for person in self.people: username = person['username'] if self.valid_user(username): uid = person['id'] human_name = person['human_name'] password = person['password'] home_dir = "%s/%s" % (config.get('users', 'home').strip('"'), username) shell = self.shell(username) passwd_file.write("=%s %s:x:%i:%i:%s:%s:%s\n" % (uid, username, uid, uid, human_name, home_dir, shell)) passwd_file.write("0%i %s:x:%i:%i:%s:%s:%s\n" % (i, username, uid, uid, human_name, home_dir, shell)) passwd_file.write(".%s %s:x:%i:%i:%s:%s:%s\n" % (username, username, uid, uid, human_name, home_dir, shell)) shadow_file.write("=%i %s:%s:99999:0:99999:7:::\n" % (uid, username, password)) shadow_file.write("0%i %s:%s:99999:0:99999:7:::\n" % (i, username, password)) shadow_file.write(".%s %s:%s:99999:0:99999:7:::\n" % (username, username, password)) i = i + 1 passwd_file.close() shadow_file.close() def valid_user_group(self, person_id): ''' Determine if person is valid on this machine as defined in the config file. I worry that this is going to be horribly inefficient with large numbers of users and groups.''' for member in self.memberships: for group in self.memberships[member]: if group['person_id'] == person_id: return True return False def get_usernames(self): usernames = {} if not self.people: self.people_list() for person in self.people: uid = person['id'] if self.valid_user_group(uid): username = person['username'] usernames[uid] = username self.usernames = usernames def get_group_mapping(self): if not self.usernames: self.get_usernames() for group in self.groups: gid = group['id'] name = group['name'] try: ''' Shoot me now I know this isn't right ''' members = [] for member in self.memberships[name]: members.append(self.usernames[member['person_id']]) memberships = ','.join(members) self.group_mapping[name] = members except KeyError: ''' No users exist in the group ''' pass def groups_text(self, groups=None, people=None): i = 0 file = open(self.temp + '/group.txt', 'w') if not self.groups: self.group_list() if not self.people: self.people_list() if not self.usernames: self.get_usernames() if not self.group_mapping: self.get_group_mapping() ''' First create all of our users/groups combo ''' for person in self.people: uid = person['id'] try: if self.valid_user(self.usernames[uid]): username = person['username'] file.write("=%i %s:x:%i:\n" % (uid, username, uid)) file.write("0%i %s:x:%i:\n" % (i, username, uid)) file.write(".%s %s:x:%i:\n" % (username, username, uid)) i = i + 1 except KeyError: continue for group in self.groups: gid = group['id'] name = group['name'] try: ''' Shoot me now I know this isn't right ''' members = [] for member in self.memberships[name]: members.append(self.usernames[member['person_id']]) memberships = ','.join(members) self.group_mapping[name] = members except KeyError: ''' No users exist in the group ''' pass file.write("=%i %s:x:%i:%s\n" % (gid, name, gid, memberships)) file.write("0%i %s:x:%i:%s\n" % (i, name, gid, memberships)) file.write(".%s %s:x:%i:%s\n" % (name, name, gid, memberships)) i = i + 1 file.close() def group_list(self, search='*'): params = {'search' : search} request = self.send_request('group/list', auth=True, input=params) self.groups = request['groups'] memberships = {} for group in self.groups: memberships[group['name']] = [] try: for member in request['memberships'][u'%s' % group['id']]: memberships[group['name']].append({'person_id': member}) except KeyError: pass self.memberships = memberships self.valid_groups() return self.groups def people_list(self, search='*'): params = {'search' : search} self.people = self.send_request('user/list', auth=True, input=params)['people'] def email_list(self, search='*'): params = {'search' : search} self.emails = self.send_request('user/email_list', auth=True, input=params)['emails'] return self.emails def make_group_db(self): self.groups_text() os.system('makedb -o %s/group.db %s/group.txt' % (self.temp, self.temp)) def make_passwd_db(self): self.passwd_text() os.system('makedb -o %s/passwd.db %s/passwd.txt' % (self.temp, self.temp)) os.system('makedb -o %s/shadow.db %s/shadow.txt' % (self.temp, self.temp)) os.chmod(self.temp + '/shadow.db', 00400) def install_passwd_db(self): try: move(self.temp + '/passwd.db', os.path.join(prefix + '/var/db/passwd.db')) except IOError, e: print "ERROR: Could not write passwd db - %s" % e def install_shadow_db(self): try: move(self.temp + '/shadow.db', os.path.join(prefix + '/var/db/shadow.db')) except IOError, e: print "ERROR: Could not write shadow db - %s" % e def install_group_db(self): try: move(self.temp + '/group.db', os.path.join(prefix + '/var/db/group.db')) except IOError, e: print "ERROR: Could not write group db - %s" % e def create_homedirs(self): ''' Create homedirs and home base dir if they do not exist ''' home_base = os.path.join(prefix + config.get('users', 'home').strip('"')) if not os.path.exists(home_base): os.makedirs(home_base, mode=0755) for person in self.people: home_dir = os.path.join(home_base, person['username']) if not os.path.exists(home_dir) and self.valid_user(person['username']): syslog.syslog('Creating homedir for %s' % person['username']) copytree('/etc/skel/', home_dir) os.path.walk(home_dir, _chown, [person['id'], person['id']]) def remove_stale_homedirs(self): ''' Remove homedirs of users that no longer have access ''' home_base = os.path.join(prefix + config.get('users', 'home').strip('"')) try: home_backup_dir = config.get('users', 'home_backup_dir').strip('"') except ConfigParser.NoOptionError: home_backup_dir = '/var/tmp/' users = os.listdir(home_base) for user in users: if not self.valid_user(user): if not os.path.exists(home_backup_dir): os.makedirs(home_backup_dir) syslog.syslog('Backed up %s to %s' % (user, home_backup_dir)) target = '%s-%s' % (user, time.mktime(datetime.datetime.now().timetuple())) move(os.path.join(home_base, user), os.path.join(prefix + home_backup_dir, target)) def create_ssh_keys(self): ''' Create ssh keys ''' home_base = prefix + config.get('users', 'home').strip('"') for person in self.people: username = person['username'] if self.valid_user(username): ssh_dir = os.path.join(home_base, username, '.ssh') if person['ssh_key']: key = self.ssh_key(person) if not os.path.exists(ssh_dir): os.makedirs(ssh_dir, mode=0700) f = codecs.open(os.path.join(ssh_dir, 'authorized_keys'), mode='w', encoding='utf-8') f.write(key + '\n') f.close() os.chmod(os.path.join(ssh_dir, 'authorized_keys'), 0600) os.path.walk(ssh_dir, _chown, [person['id'], person['id']]) def make_aliases_txt(self): ''' update your mail aliases file ''' if not self.groups: groups = self.group_list() self.emails = self.email_list() email_file = codecs.open(self.temp + '/aliases', mode='w', encoding='utf-8') email_template = codecs.open(config.get('host', 'aliases_template').strip('"')) email_file.write("# Generated by fasClient\n") for line in email_template.readlines(): email_file.write(line) sorted = self.emails.keys() sorted.sort() for person in sorted: email_file.write("%s: %s\n" % (person, self.emails[person])) usernames = self.usernames() for group in self.groups: name = group['name'] members = {} members['member'] = [] for membership in self.memberships[name]: role_type = membership['role_type'] person = usernames[membership['person_id']] if role_type == 'user': ''' Legacy support ''' members['member'].append(person) continue members['member'].append(person) try: members[role_type].append(person) except KeyError: members[role_type] = [person] for role in members: email_file.write("%s-%ss: %s\n" % (name, role, ','.join(members[role]))) email_file.close() def enable(): temp = tempfile.mkdtemp('-tmp', 'fas-', config.get('global', 'temp').strip('"')) old = open('/etc/sysconfig/authconfig', 'r') new = open(temp + '/authconfig', 'w') for line in old: if line.startswith("USEDB"): new.write("USEDB=yes\n") else: new.write(line) new.close() old.close() try: move(temp + '/authconfig', '/etc/sysconfig/authconfig') except IOError, e: print "ERROR: Could not write /etc/sysconfig/authconfig - %s" % e sys.exit(5) os.system('/usr/sbin/authconfig --updateall') rmtree(temp) def disable(): temp = tempfile.mkdtemp('-tmp', 'fas-', config.get('global', 'temp').strip('"')) old = open('/etc/sysconfig/authconfig', 'r') new = open(temp + '/authconfig', 'w') for line in old: if line.startswith("USEDB"): new.write("USEDB=no\n") else: new.write(line) old.close() new.close() try: move(temp + '/authconfig', '/etc/sysconfig/authconfig') except IOError, e: print "ERROR: Could not write /etc/sysconfig/authconfig - %s" % e sys.exit(5) os.system('/usr/sbin/authconfig --updateall') rmtree(temp) if __name__ == '__main__': if opts.enable: enable() if opts.disable: disable() if opts.install: try: fas = MakeShellAccounts(FAS_URL, config.get('global', 'login').strip('"'), config.get('global', 'password').strip('"'), False) except AuthError, e: print >> sys.stderr, e sys.exit(1) fas.mk_tempdir() fas.make_group_db() fas.make_passwd_db() if not opts.no_group: fas.install_group_db() if not opts.no_passwd: fas.install_passwd_db() if not opts.no_shadow: fas.install_shadow_db() if not opts.no_home_dirs: fas.create_homedirs() fas.remove_stale_homedirs() if not opts.no_ssh_keys: fas.create_ssh_keys() fas.rm_tempdir() if opts.aliases: try: fas = MakeShellAccounts(FAS_URL, config.get('global', 'login').strip('"'), config.get('global', 'password').strip('"'), False) except AuthError, e: print >> sys.stderr, e sys.exit(1) fas.mk_tempdir() fas.make_aliases_txt() fas.install_aliases_txt() if not (opts.install or opts.enable or opts.disable or opts.aliases): parser.print_help()