568 lines
23 KiB
Python
Executable file
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()
|