fedora-infrastructure/fas/client/fasClient
2008-03-11 18:33:14 -05:00

568 lines
23 KiB
Python
Executable file

#!/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 <mmcgrath@redhat.com>
#
# 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()