This is the new fasClient. It almost works right.
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright © 2007-2009 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
# Toshio Kuratomi
# Ricky Zhou
import os
import sys
import codecs
import tempfile
import logging
import syslog
import datetime
import subprocess
import time
from fedora.client import AccountSystem, AuthError, ServerError
try:
import cPickle as pickle
except ImportError:
import pickle
import ConfigParser
from optparse import OptionParser
from shutil import move, rmtree, copytree
from rhpl.translate import _
parser = OptionParser()
parser.add_option('-i', '--install',
dest = 'install',
default = False,
action = 'store_true',
help = _('Download and sync most recent content'))
parser.add_option('-I', '--info',
dest = 'info_username',
default = False,
metavar = 'info_username',
help = _('Get info about a user'))
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'))
parser.add_option('--nosession',
dest = 'nosession',
default = False,
action = 'store_true',
help = _('Disable the creation of ~/.fedora_session'))
parser.add_option('--debug',
dest = 'debug',
default = False,
action = 'store_true',
help = _('Enable debugging messages'))
(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(AccountSystem):
_users = None
_groups = None
_good_users = None
_group_types = None
_temp = None
def _make_tempdir(self, force=False):
'''Return a temporary directory'''
if not self._temp or force:
# Remove any existing temp directories
if self._temp:
rmtree(self._temp)
self._temp = tempfile.mkdtemp('-tmp', 'fas-', config.get('global', 'temp').strip('"'))
return self._temp
temp = property(_make_tempdir)
def _refresh_users(self, force=False):
'''Return a list of users in FAS'''
# Cached values present, return
if not self._users or force:
self._users = self.user_data()
return self._users
users = property(_refresh_users)
def _refresh_groups(self, force=False):
'''Return a list of groups in FAS'''
# Cached values present, return
if not self._groups or force:
group_data = self.group_data()
# The JSON output from FAS encodes dictionary keys as strings, but leaves
# array elements as integers (in the case of group member UIDs). This
# normalizes them to all strings.
for group in group_data:
for role_type in ('administrators', 'sponsors', 'users'):
group_data[group][role_type] = [str(uid) for uid in group_data[group][role_type]]
self._groups = group_data
return self._groups
groups = property(_refresh_groups)
def _refresh_good_users_group_types(self, force=False):
# Cached values present, return
if self._good_users and self._group_types and not force:
return
cla_group = config.get('global', 'cla_group').strip('"')
if cla_group not in self.groups:
print >> sys.stderr, 'No such group: %s' % cla_group
print >> sys.stderr, 'Aborting.'
sys.exit(1)
cla_uids = self.groups[cla_group]['users'] + \
self.groups[cla_group]['sponsors'] + \
self.groups[cla_group]['administrators']
user_groupcount = {}
group_types = {}
for uid in cla_uids:
user_groupcount[uid] = 0
for group in self.groups:
group_type = self.groups[group]['type']
if group.startswith('cla_'):
continue
for uid in self.groups[group]['users'] + \
self.groups[group]['sponsors'] + \
self.groups[group]['administrators']:
if group_type not in group_types:
group_types[group_type] = set()
group_types[group_type].add(uid)
if uid in user_groupcount:
user_groupcount[uid] += 1
good_users = set()
for uid in user_groupcount:
# If the user is active, has signed a CLA, and is in at least one
# other group, add them to good_users.
if uid in self.users and user_groupcount[uid] > 0:
good_users.add(uid)
self._good_users = good_users
self._group_types = group_types
def _refresh_good_users(self, force=False):
'''Return a list of users in who have CLA + 1 group'''
self._refresh_good_users_group_types(force)
return self._good_users
good_users = property(_refresh_good_users)
def _refresh_group_types(self, force=False):
'''Return a list of users in group with various types'''
self._refresh_good_users_group_types(force)
return self._group_types
group_types = property(_refresh_group_types)
def filter_users(self, valid_groups=None, restricted_groups=None):
'''Return a list of users who get normal and restricted accounts on a machine'''
if valid_groups is None:
valid_groups = []
if restricted_groups is None:
restricted_groups = []
all_groups = valid_groups + restricted_groups
users = {}
for group in all_groups:
uids = set()
restricted = group not in valid_groups
if group.startswith('@'):
# Filter by group type
group_type = group[1:]
if group_type == 'all':
# It's all good as long as a the user is in CLA + one group
uids.update(self.good_users)
else:
if group_type not in self.group_types:
print >> sys.stderr, 'No such group type: %s' % group_type
continue
uids.update(self.group_types[group_type])
else:
if group not in self.groups:
print >> sys.stderr, 'No such group: %s' % group
continue
uids.update(self.groups[group]['users'])
uids.update(self.groups[group]['sponsors'])
uids.update(self.groups[group]['administrators'])
for uid in uids:
if uid not in self.users:
# The user is most likely inactive.
continue
if restricted:
# Make sure that the most privileged group wins.
if uid not in users:
users[uid] = {}
users[uid]['shell'] = config.get('users', 'shell').strip('"')
users[uid]['ssh_cmd'] = config.get('users', 'ssh_restricted_app').strip('"')
users[uid]['ssh_options'] = config.get('users', 'ssh_key_options').strip('"')
else:
users[uid] = {}
users[uid]['shell'] = config.get('users', 'ssh_restricted_shell').strip('"')
users[uid]['ssh_cmd'] = ''
users[uid]['ssh_options'] = ''
return users
def passwd_text(self, users):
'''Create the text password file'''
i = 0
home_dir_base = config.get('users', 'home').strip('"')
# Touch shadow and secure the permissions
shadow_file = codecs.open(os.path.join(self.temp, 'shadow.txt'), mode='w', encoding='utf-8')
shadow_file.close()
os.chmod(os.path.join(self.temp, 'shadow.txt'), 00600)
passwd_file = codecs.open(os.path.join(self.temp, 'passwd.txt'), mode='w', encoding='utf-8')
shadow_file = codecs.open(os.path.join(self.temp, 'shadow.txt'), mode='w', encoding='utf-8')
for uid in users:
username = self.users[uid]['username']
human_name = username
password = self.users[uid]['password']
home_dir = '%s/%s' % (home_dir_base, username)
shell = users[uid]['shell']
passwd_file.write('=%s %s:x:%s:%s:%s:%s:%s\n' % (uid, username, uid, uid, human_name, home_dir, shell))
passwd_file.write('0%i %s:x:%s:%s:%s:%s:%s\n' % (i, username, uid, uid, human_name, home_dir, shell))
passwd_file.write('.%s %s:x:%s:%s:%s:%s:%s\n' % (username, username, uid, uid, human_name, home_dir, shell))
shadow_file.write('=%s %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 += 1
passwd_file.close()
shadow_file.close()
def groups_text(self, users):
'''Create the text groups file'''
i = 0
group_file = codecs.open(os.path.join(self.temp, 'group.txt'), 'w')
# First create all of our users/groups combo
# Only create user groups for users that actually exist on the system
for uid in users:
username = self.users[uid]['username']
group_file.write('=%s %s:x:%s:\n' % (uid, username, uid))
group_file.write('0%i %s:x:%s:\n' % (i, username, uid))
group_file.write('.%s %s:x:%s:\n' % (username, username, uid))
i += 1
for group in self.groups:
gid = self.groups[group]['id']
members = set()
memberships = ''
for member_uid in self.groups[group]['administrators'] + \
self.groups[group]['sponsors'] + \
self.groups[group]['users']:
try:
members.add(self.users[member_uid]['username'])
except KeyError:
# This means that the user is most likely disabled.
pass
memberships = ','.join(members)
group_file.write('=%i %s:x:%i:%s\n' % (gid, group, gid, memberships))
group_file.write('0%i %s:x:%i:%s\n' % (i, group, gid, memberships))
group_file.write('.%s %s:x:%i:%s\n' % (group, group, gid, memberships))
i += 1
group_file.close()
def make_group_db(self, users):
'''Compile the groups file'''
self.groups_text(users)
subprocess.call(['/usr/bin/makedb', '-o', os.path.join(self.temp, 'group.db'), os.path.join(self.temp, 'group.txt')])
def make_passwd_db(self, users):
'''Compile the password and shadow files'''
self.passwd_text(users)
subprocess.call(['/usr/bin/makedb', '-o', os.path.join(self.temp, 'passwd.db'), os.path.join(self.temp, 'passwd.txt')])
subprocess.call(['/usr/bin/makedb', '-o', os.path.join(self.temp, 'shadow.db'), os.path.join(self.temp, 'shadow.txt')])
os.chmod(os.path.join(self.temp, 'shadow.db'), 00400)
os.chmod(os.path.join(self.temp, 'shadow.txt'), 00400)
def make_aliases_text(self):
'''Create the aliases file'''
email_file = codecs.open(os.path.join(self.temp, 'aliases'), mode='w', encoding='utf-8')
try:
email_template = codecs.open(config.get('host', 'aliases_template').strip('"'))
except IOError, e:
print >> sys.stderr, 'Could not open aliases template %s: %s' % (config.get('host', 'aliases_template').strip('"'), e)
print >> sys.stderr, 'Aborting.'
sys.exit(1)
email_file.write('# Generated by fasClient\n')
for line in email_template.readlines():
email_file.write(line)
for uid in self.good_users:
email_file.write('%s: %s\n' % (self.users[uid]['username'], self.users[uid]['email']))
for group in self.groups:
administrators = set()
sponsors = set()
members = set()
for uid in self.groups[group]['users']:
if uid in self.good_users:
# The user has an @fedoraproject.org alias
username = self.users[uid]['username']
members.add(username)
else:
# Add their email if they aren't disabled.
if uid in self.users:
members.add(self.users[uid]['email'])
for uid in self.groups[group]['sponsors']:
if uid in self.good_users:
# The user has an @fedoraproject.org alias
username = self.users[uid]['username']
sponsors.add(username)
members.add(username)
else:
# Add their email if they aren't disabled.
if uid in self.users:
sponsors.add(self.users[uid]['email'])
members.add(self.users[uid]['email'])
for uid in self.groups[group]['administrators']:
if uid in self.good_users:
# The user has an @fedoraproject.org alias
username = self.users[uid]['username']
administrators.add(username)
sponsors.add(username)
members.add(username)
else:
# Add their email if they aren't disabled.
if uid in self.users:
administrators.add(self.users[uid]['email'])
sponsors.add(self.users[uid]['email'])
members.add(self.users[uid]['email'])
if administrators:
email_file.write('%s-administrators: %s\n' % (group, ','.join(administrators)))
if sponsors:
email_file.write('%s-sponsors: %s\n' % (group, ','.join(sponsors)))
if members:
email_file.write('%s-members: %s\n' % (group, ','.join(members)))
def create_home_dirs(self, users, modes=None):
''' Create homedirs and home base dir if they do not exist '''
if modes is None:
modes = {}
home_dir_base = os.path.join(prefix, config.get('users', 'home').strip('"').lstrip('/'))
if not os.path.exists(home_dir_base):
os.makedirs(home_dir_base, mode=0755)
for uid in users:
username = self.users[uid]['username']
home_dir = os.path.join(home_dir_base, username)
if not os.path.exists(home_dir):
syslog.syslog('Creating homedir for %s' % username)
copytree('/etc/skel/', home_dir)
os.path.walk(home_dir, _chown, [int(uid), int(uid)])
else:
dir_stat = os.stat(home_dir)
if dir_stat.st_uid == 0:
if username in modes:
os.chmod(home_dir, modes[username])
else:
os.chmod(home_dir, 00755)
os.path.walk(home_dir, _chown, [int(uid), int(uid)])
def remove_stale_homedirs(self, users):
''' Remove homedirs of users that no longer have access '''
home_dir_base = os.path.join(prefix, config.get('users', 'home').strip('"').lstrip('/'))
home_backup_dir = os.path.join(prefix, config.get('users', 'home_backup_dir').strip('"').lstrip('/'))
valid_users = [self.users[uid]['username'] for uid in users]
current_users = os.listdir(home_dir_base)
modes = {}
for user in current_users:
if user not in valid_users:
home_dir = os.path.join(home_dir_base, user)
dir_stat = os.stat(home_dir)
if dir_stat.st_uid != 0:
modes[user] = dir_stat.st_mode
syslog.syslog('Locking permissions on %s' % home_dir)
os.path.walk(home_dir, _chown, [0, 0])
os.chmod(home_dir, 00700)
return modes
def create_ssh_keys(self, users):
''' Create SSH keys '''
home_dir_base = os.path.join(prefix, config.get('users', 'home').strip('"').lstrip('/'))
for uid in users:
username = self.users[uid]['username']
ssh_dir = os.path.join(home_dir_base, username, '.ssh')
if self.users[uid]['ssh_key']:
if users[uid]['ssh_cmd'] or users[uid]['ssh_options']:
key = 'command="%s",%s %s' % (users[uid]['ssh_cmd'], users[uid]['ssh_options'], self.users[uid]['ssh_key'])
else:
key = self.users[uid]['ssh_key']
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, [int(uid), int(uid)])
def install_passwd_db(self):
'''Install the password database'''
try:
move(os.path.join(self.temp, 'passwd.db'), os.path.join(prefix, 'var/db/passwd.db'))
except IOError, e:
print 'ERROR: Could not install passwd db: %s' % e
def install_shadow_db(self):
'''Install the shadow database'''
try:
move(os.path.join(self.temp, 'shadow.db'), os.path.join(prefix, 'var/db/shadow.db'))
except IOError, e:
print 'ERROR: Could not install shadow db: %s' % e
def install_group_db(self):
'''Install the group database'''
try:
move(os.path.join(self.temp, 'group.db'), os.path.join(prefix, 'var/db/group.db'))
except IOError, e:
print 'ERROR: Could not install group db: %s' % e
def install_aliases(self):
'''Install the aliases file'''
move(os.path.join(self.temp, 'aliases'), os.path.join(prefix, 'etc/aliases'))
subprocess.call(['/usr/bin/newaliases'])
def user_info(self, username):
'''Print information on a user'''
person = self.person_by_username(username)
if not person:
print 'No such person: %s' % username
return
print 'User: %s' % person['username']
print ' Name: %s' % person['human_name']
print ' Created: %s' % person['creation'].split(' ')[0]
print ' Timezone: %s' % person['timezone']
print ' IRC Nick: %s' % person['ircnick']
print ' Locale: %s' % person['locale']
print ' Status: %s' % person['status']
print ' Approved Groups: '
if person['approved_memberships']:
for group in person['approved_memberships']:
print ' %s' % group['name']
else:
print ' None'
print ' Unapproved Groups: '
if person['unapproved_memberships']:
for group in person['unapproved_memberships']:
print ' %s' % group['name']
else:
print ' None'
def cleanup(self):
'''Perform any necessary cleanup tasks'''
if self.temp:
rmtree(self.temp)
def enable():
'''Enable FAS authentication'''
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(os.path.join(temp, 'authconfig'), '/etc/sysconfig/authconfig')
except IOError, e:
print 'ERROR: Could not write /etc/sysconfig/authconfig: %s' % e
sys.exit(5)
subprocess.call(['/usr/sbin/authconfig', '--updateall'])
rmtree(temp)
def disable():
'''Disable FAS authentication'''
temp = tempfile.mkdtemp('-tmp', 'fas-', config.get('global', 'temp').strip('"'))
old = open('/etc/sysconfig/authconfig', 'r')
new = open(os.path.join(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(os.path.join(temp, 'authconfig'), '/etc/sysconfig/authconfig')
except IOError, e:
print 'ERROR: Could not write /etc/sysconfig/authconfig: %s' % e
sys.exit(5)
subprocess.call(['/usr/sbin/authconfig', '--updateall'])
rmtree(temp)
if __name__ == '__main__':
if not (opts.install or opts.enable or opts.disable or opts.aliases or opts.info_username):
parser.print_help()
sys.exit(0)
if opts.enable:
enable()
if opts.disable:
disable()
try:
fas = MakeShellAccounts(FAS_URL, username=config.get('global', 'login').strip('"'), password=config.get('global', 'password').strip('"'), debug=opts.debug)
except AuthError, e:
sys.stderr.write('%s\n' % str(e))
sys.exit(1)
except URLError, e:
sys.stderr.write('Could not connect to %s: %s\n' % (FAS_URL, e.reason[1]))
sys.exit(9)
valid_groups = []
restricted_groups = []
valid_grouplist = config.get('host', 'groups').strip('"')
restricted_grouplist = config.get('host', 'ssh_restricted_groups').strip('"')
if valid_grouplist:
valid_groups = valid_grouplist.split(',')
if restricted_grouplist:
restricted_groups = restricted_grouplist.split(',')
if opts.info_username:
fas.user_info(opts.info_username)
if opts.install:
users = fas.filter_users(valid_groups=valid_groups, restricted_groups=restricted_groups)
fas.make_group_db(users)
fas.make_passwd_db(users)
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:
try:
modefile = open(config.get('global', 'modefile'), 'r')
modes = pickle.load(modefile)
except IOError:
modes = {}
else:
modefile.close()
fas.create_home_dirs(users, modes=modes)
new_modes = fas.remove_stale_homedirs(users)
modes.update(new_modes)
try:
modefile = open(config.get('global', 'modefile'), 'w')
pickle.dump(modes, modefile)
except IOError:
pass
else:
modefile.close()
if not opts.no_ssh_keys:
fas.create_ssh_keys(users)
if opts.aliases:
fas.make_aliases_text()
fas.install_aliases()
fas.cleanup()
If you spot the bug, let me know.