fedora-infrastructure/fas/client/fasClient

495 lines
20 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('-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('"')
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 = {}
def mk_tempdir(self):
self.temp = tempfile.mkdtemp('-tmp', 'fas-', config.get('global', 'temp').strip('"'))
def rm_tempdir(self):
rmtree(self.temp)
def valid_user(self, username):
valid_groups = config.get('host', 'groups').strip('"').split(',') + \
config.get('host', 'restricted_groups').strip('"').split(',') + \
config.get('host', 'ssh_restricted_groups').strip('"').split(',')
try:
for group in valid_groups:
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 config.get('host', 'groups').strip('"').split(','):
try:
if person['username'] in self.group_mapping[group]:
return person['ssh_key']
except KeyError:
print >> sys.stderr, '%s is could not be found in fas but was in your config under "groups"!' % group
continue
for group in config.get('host', 'restricted_groups').strip('"').split(','):
try:
if person['username'] in self.group_mapping[group]:
return person['ssh_key']
except KeyError:
print >> sys.stderr, '%s is could not be found in fas but was in your config under "restricted_groups"!' % group
continue
for group in config.get('host', 'ssh_restricted_groups').strip('"').split(','):
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 KeyError:
print >> sys.stderr, '%s is 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 config.get('host', 'groups').strip('"').split(','):
try:
if username in self.group_mapping[group]:
return config.get('users', 'shell').strip('"')
except KeyError:
print >> sys.stderr, '%s is could not be found in fas but was in your config under "groups"!' % group
continue
for group in config.get('host', 'restricted_groups').strip('"').split(','):
try:
if username in self.group_mapping[group]:
return config.get('users', 'restricted_shell').strip('"')
except KeyError:
print >> sys.stderr, '%s is could not be found in fas but was in your config under "restricted_groups"!' % group
continue
for group in config.get('host', 'ssh_restricted_groups').strip('"').split(','):
try:
if username in self.group_mapping[group]:
return config.get('users', 'ssh_restricted_shell').strip('"')
except KeyError:
print >> sys.stderr, '%s is could not be found in fas but was in your config under "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', '/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 = 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 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
return usernames
def groups_text(self, groups=None, people=None):
i = 0
file = open(self.temp + '/group.txt', 'w')
if not groups:
groups = self.group_list()
if not people:
people = self.people_list()
''' First create all of our users/groups combo '''
for person in people:
uid = person['id']
if self.valid_user_group(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
usernames = self.usernames()
for group in 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(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']
self.memberships = request['memberships']
return self.groups
def people_list(self, search='*'):
params = {'search' : search}
self.people = self.send_request('user/list', auth=True, input=params)['people']
return self.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', '/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', '/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', '/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 = 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 = 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(home_backup_dir, target))
def create_ssh_keys(self):
''' Create ssh keys '''
home_base = 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 = open(os.path.join(ssh_dir, 'authorized_keys'), 'w')
f.write(key)
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()