#!/usr/bin/env python
# Create authorized_keys file for each system user
# 
# Copyright (C) 2009, 2011  Sylvain Beucler
#
# This file is part of Savane.
# 
# Savane is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# 
# Savane is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import ConfigParser
import os, sys
import pwd
import subprocess


from optparse import OptionParser
parser = OptionParser()
parser.add_option("-d", "--single-dir", metavar="DIR",
                  help="""Instead of storing keys files in
~/.ssh/authorized_keys, put them all in DIR, in a file named after the
user.  For example: --single-dir=/etc/ssh/authorized_keys/ . In that
case, for OpenSSH, you would use 'AuthorizedKeysFile
/etc/ssh/authorized_keys/%u' in /etc/ssh/sshd_config . It is assumed that
the directory is not writable by unprivileged users, to avoid symlink attacks.""")
parser.add_option("-s", "--no-user-notification",
                  help="""Don't notify user by e-mail when SSH key is changed (decreases security)""")
(options, args) = parser.parse_args()

use_homedir = True
if options.single_dir is not None:
    use_homedir = False


cp = ConfigParser.RawConfigParser()
cp.read('/etc/savane/savane.ini')

import MySQLdb
MySQLdb.charset = 'UTF-8'
conn = MySQLdb.connect(host=cp.get('database', 'HOST'),
                       user=cp.get('database', 'USER'),
                       passwd=cp.get('database', 'PASSWORD'),
                       db=cp.get('database', 'NAME'),
                       use_unicode=True)

# Get a list of users that are members of a project
conn.query("""
SELECT user_name, LOWER(CONCAT('/home/', SUBSTRING(user_name FROM 1 FOR 1),
    '/', SUBSTRING(user_name FROM 1 FOR 2),
    '/', user_name)) AS homedir, authorized_keys, email, realname
FROM user
  JOIN user_group ON user.user_id = user_group.user_id
  JOIN groups ON user_group.group_id = groups.group_id
WHERE uidNumber >= 1000
  AND user.status = 'A'
  AND user_group.admin_flags <> 'P'
  AND groups.status = 'A'
GROUP BY user_group.user_id
  HAVING count(user_group.group_id) > 0
""");
res = conn.store_result()

# By making each user own his 'authorized_keys' files, we can prevent
# other users from looking at each others' keys, which increases
# security as the attacker can't brute-force them.
old_umask = os.umask(077)

# The naive approach is to overwrite all authorized_keys files on
# disk. In practice this mean lots of disk write. It's more efficient
# to read all existing files and overwrite them only if there's a
# difference!
for row in res.fetch_row(maxrows=0, how=1):
    db_keys = row['authorized_keys'] or ''
    db_keys = db_keys.replace('###', "\n")
    db_keys = db_keys.encode('UTF-8')

    sys_keys = ''
    user_sshdir_path = row['homedir'] + '/.ssh'
    if use_homedir:
        key_path = user_sshdir_path + '/authorized_keys'
    else:
        key_path = os.path.normpath(options.single_dir) + '/' + row['user_name']

    if os.path.exists(key_path):
        f = open(key_path)
        sys_keys = f.read()
        f.close()

    if sys_keys != db_keys:
        ent = pwd.getpwnam(row['user_name'])
        uid = ent[2]
        gid = ent[3]

        if use_homedir:
            # Avoid symlink attacks and races (and avoid calling chown)
            # http://en.wikipedia.org/wiki/Symlink_race
            groups_save = os.getgroups()
            os.setgroups([])
            os.setegid(gid)
            os.seteuid(uid)
            if not os.path.exists(user_sshdir_path):
                os.mkdir(user_sshdir_path, 0700)
            if os.path.islink(user_sshdir_path) or os.path.islink(key_path):
                # Note: even when checking for symlinks, seteuid is
                # necessary, to avoid symlinks race attacks.
                print >> sys.stderr, "Possible symlink attack for %s!" % (key_path)
                continue
        else:
            # Assume the directory is not writable by unprivileged
            # users
            pass

        f = open(key_path, 'w')
        f.write(db_keys)
        f.close()

        if use_homedir:
            # Switch back to root
            os.seteuid(0)
            os.setegid(0)
            os.setgroups(groups_save)
        else:
            os.chown(key_path, uid, gid)

        if not options.no_user_notification:
            # Notify impacted user by e-mail.
            # This increases security because if a user web account is
            # compromised, changes in the SSH keys will be noticed.
            p = subprocess.Popen(['sendmail', '-t'], stdin=subprocess.PIPE)
            p.stdin.write("""From: "Savane robot" <root>\n""")
            p.stdin.write("""To: "%s" <%s>\n""" % (row['realname'].replace('"', '\\"'), row['email']))
            p.stdin.write("Subject: SSH authorized_keys file update\n")
            p.stdin.write("\n")
            p.stdin.write(("Your SSH authorized_keys file was just updated on the '%s' system.\n"
                           "\n"
                           "If you didn't request a change in your SSH authorized_keys,"
                           " somebody may have cracked your account,"
                           " in which case please notify the system administrators.\n")
                          % subprocess.Popen(['hostname'], stdout=subprocess.PIPE).communicate()[0].strip())
            p.stdin.close()
            if p.wait() != 0:
                print "Error sending mail to %s" % row['email']
            
os.umask(old_umask)
