Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite pythonic sorry #179

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
361 changes: 206 additions & 155 deletions staff/acct/sorry/sorry
Original file line number Diff line number Diff line change
@@ -1,155 +1,206 @@
#!/bin/bash
# Script for sorrying OCF user accounts

REASONPATH=/opt/share/utils/staff/acct/sorry
LDAPSEARCH=$(command -v ldapsearch)
LDAPMODIFY=$(command -v ldapmodify)
KINIT=$(command -v kinit)
KDESTROY=$(command -v kdestroy)
#KRB5CCNAME=/root/krb5cc_sorry
#export KRB5CCNAME

# check to see if running as root
if [ "$(/usr/bin/id -u)" != 0 ]; then
echo "You must be root to run this."
exit 2
fi

if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 [user to be sorried] [sorry reason file]"
echo
echo "Standard Sorry Reasons:"
find "$REASONPATH" -maxdepth 1 -mindepth 1 -type f -printf " %f\\n"
echo
echo "For custom sorry reasons you can pass in your own file"
exit 0
fi

sorriedUser=$1

if [[ ! "$sorriedUser" =~ ^[a-z0-9]+ ]]; then
echo "$sorriedUser is an invalid username"
exit 3
fi

if [ -z "$(getent passwd "$sorriedUser")" ]; then
echo "User $sorriedUser does not exist"
exit 3
fi

sorryFile=$2

if [ ! -f "$sorryFile" ]; then
sorryFile="$REASONPATH/$sorryFile"
if [ ! -f "$sorryFile" ]; then
echo "Invalid sorry file"
exit 3
fi
fi

userdir=$(ldapsearch -x uid="$sorriedUser" | grep homeDirectory | cut -d' ' -f2)

# Rewriting this to use bash variable substitution is annoying and confusing, so
# just use sed instead and ignore the shellcheck warning
# shellcheck disable=SC2001
httpdir=$(echo "$sorriedUser" | sed -E 's%([a-z])[a-z]*%/services/http/users/\1/\0%')

rootstaffer="$SUDO_USER"

if [ "$rootstaffer" = "root" ] || [ -z "$rootstaffer" ]; then
echo "The sorry.log is much more useful when it logs who you are"
echo "rather than simply 'root'. Please enter your username:"
read -r rootstaffer
fi

if [ -z "$SORRY_KRB5CCNAME" ]; then
echo "You are $rootstaffer"
if ! $KINIT "${rootstaffer}/admin"; then
echo "kinit failed, bailing out!"
exit 1
fi
else
echo "SORRY_KRB5CCNAME set in environment."
echo "Assuming this file contains current admin credentials."
KRB5CCNAME="$SORRY_KRB5CCNAME"
export KRB5CCNAME
fi

sorryshell=/opt/share/utils/bin/sorried

echo ""
echo "Copying sorry file and making .oldshell file"

oldshell=$($LDAPSEARCH -x "(uid=$sorriedUser)" loginShell | grep "^loginShell:" | cut -d" " -f2)

if [[ ! -f .oldshell ]]; then
echo "$oldshell" > "$userdir/.oldshell"
fi

cp "$sorryFile" "$userdir/.sorry"
chmod 400 "$userdir/.sorry"
chown "$sorriedUser:ocf" "$userdir/.sorry"

echo ""
echo "Changing user's shell to a sorry shell"

sorrygid=$(getent group sorry | cut -d : -f 3)
$LDAPMODIFY -H ldaps://ldap.ocf.berkeley.edu <<EOF
dn: uid=$sorriedUser,ou=People,dc=OCF,dc=Berkeley,dc=EDU
changetype: modify
replace: loginShell
loginShell: $sorryshell
-
replace: gidNumber
gidNumber: $sorrygid
EOF

echo "Changing permissions on httpdir to 000"
if [[ -d "$httpdir" ]]; then
chmod 000 "$httpdir"
fi

echo ""
echo "Changing permissions on home directory to 500"
chmod 500 "$userdir"

echo ""
echo ""
echo ""
echo "Final system check"
ldapsearch -x uid="$sorriedUser"
ldapsearch -x cn=sorry | tail
finger -m "$sorriedUser"
ls -la "$userdir"
ls -ld "$httpdir"

# logging
echo "$(/bin/date) - $rootstaffer $sorriedUser" >> /opt/acct/sorry.log

# Notify user by email if email address is available.
email_from='Open Computing Facility <[email protected]>'
email_subject='[OCF] Account disabled'
email_to="$(ldapsearch "uid=${sorriedUser}" mail | grep ^mail | cut -d' ' -f2)"
[ -n "$email_to" ] && \
mail -a "From: ${email_from}" -s "$email_subject" "$email_to" <<EOF
Hello,

Your OCF account has been disabled.

Your account name is: ${sorriedUser}

The reason your account has been disabled:
$(cat "$sorryFile")

Feel free to reply to this message.
Please do not share your password with anyone or over email.

$(python3 -c 'import ocflib.misc.mail as mail; print(mail.MAIL_SIGNATURE)')
EOF

if [ -z "$SORRY_KRB5CCNAME" ]; then
$KDESTROY
fi

# Huge notice
echo "NOW RUN note -u $sorriedUser reason. NOW"
#!/usr/bin/env python3
# Script for sorrying OCF accounts v2
import argparse
import datetime
import grp
import json
import os
import pathlib
import shutil
import subprocess
import sys
import textwrap

import ocflib.account.manage
import ocflib.account.search
import ocflib.account.utils
import ocflib.misc.mail

REASON_PATH = pathlib.Path('/opt/share/utils/staff/acct/sorry/')
SORRY_SHELL = pathlib.Path('/opt/share/utils/bin/sorried')
# TODO: This sorry log file does not exist anymore, do we still log to file?
SORRY_LOG = pathlib.Path('/opt/acct/sorry.log')
kerberos_new_ticket = True

def find_sorry_file(file_path):
# Own sorry file
if os.path.isfile(file_path):
return file_path

# Default sorry files
default_sorry_file = os.path.join(REASON_PATH, file_path)
if os.path.isfile(default_sorry_file):
return default_sorry_file

raise FileNotFoundError('Sorry file "{}" does not exist.'.format(file_path))

# Returns the real user who is running this script, but probably only works on Linux
def real_user():
if 'SUDO_USER' in os.environ and os.environ['SUDO_USER'] != 'root':
return os.environ['SUDO_USER']
else:
print("The sorry.log is much more useful when it logs who you are rather than simply 'root'.")
username = input('Please enter your username: ')
return username

# Initiates a kerberos ticket for current user
# Returns True when a new ticket is created via kinit so that we destroy it afterwards
# Raises OSError is kinit screwed up
def kerberos_init(staff_name):
if ('SORRY_KRB5CCNAME' in os.environ):
print('SORRY_KRB5CCNAME set in environment. Assuming this file contains current admin credentials.')
os.environ['KRB5CCNAME'] = os.environ['SORRY_KRB5CCNAME']
return False
else:
print('You are {0}.'.format(staff_name))
klist = subprocess.run(['sudo', '-u', staff_name, 'klist', '--json'], stdout=subprocess.PIPE)
if klist.returncode == 0:
cache_info = json.loads(klist.stdout.decode())
if cache_info.get('principal') == '{0}/[email protected]'.format(staff_name):
print('Using your preexisting credentials.')
return False

if (subprocess.call(['kinit', '{0}/admin'.format(staff_name)]) != 0):
raise OSError('Kinit failed, bailing out!')
else:
print('Kinit complete.')
return True

def kerberos_destroy():
return subprocess.call(['kdestroy'])

def write_log_file(staff_username: str, sorried_user: str):
with open(SORRY_LOG, 'a') as log_file:
# Date style matters...?
log_file.write('{0} - {1} {2}'.format(str(datetime.datetime.now()), staff_username, sorried_user))

LOG_EMAIL_BODY = textwrap.dedent("""
This is an automated email sent by OCF sorry script. This is the log of a recent sorry operation:
On "{3}", account "{0}" has been disabled by "{1}" for reason:
"{2}".
""")

SORRY_EMAIL_BODY = textwrap.dedent("""
Hello,

Your OCF account has been disabled.

Your account name is: {0}

The reason your account has been disabled is:
{1}

Feel free to reply to this message.
Please do not share your password with anyone or over email.

{2}
""")

# From utils/acct/update-email, move to ocflib LATER
def get_email(username):
"""Returns current email, or None."""
LDAP_MAIL_ATTR = 'mail'
# Since the mail attribute is private, and we can't get the attribute's
# value without authenticating, we have to use ldapsearch here instead of
# something like ldap3.
output = subprocess.check_output(
('ldapsearch', '-LLL', 'uid={}'.format(username), LDAP_MAIL_ATTR),
stderr=subprocess.DEVNULL,
).decode('utf-8').split('\n')

mail_attr = [attr for attr in output if attr.startswith(LDAP_MAIL_ATTR + ': ')]

if mail_attr:
# Strip the '{LDAP_MAIL_ATTR}: ' from the beginning of the string
return mail_attr[0][len(LDAP_MAIL_ATTR) + 2:].strip()

def send_log_mail(staff_username: str, sorried_user: str, reason: str):
# Use root@ for now
email_from = 'Open Computing Facility <[email protected]>'
email_subject = '[OCF] Account disabled'
email_body = LOG_EMAIL_BODY.format(sorried_user, staff_username, reason, str(datetime.datetime.now()))
ocflib.misc.mail.send_mail('[email protected]', email_subject, email_body, cc=None, sender=email_from)

def send_sorry_mail(sorried_user_email: str, username: str, reason: str):
email_subject = '[OCF] Account disabled'
email_body = SORRY_EMAIL_BODY.format(username, reason, ocflib.misc.mail.MAIL_SIGNATURE)
ocflib.misc.mail.send_mail(sorried_user_email, email_subject, email_body, cc=None)

def sorry_user(username: str, sorry_file):
attributes = ocflib.account.search.user_attrs(username)
if attributes is None:
raise ValueError('User {0} not found.'.format(username))

staff_username = real_user()
kerberos_new_ticket = kerberos_init(staff_username)

user_dir = attributes['homeDirectory']

print('Copying sorry file and making .oldshell file')

if not os.path.isfile(user_dir + '/.oldshell'):
with open(user_dir + '/.oldshell', 'w') as oldshell_file:
oldshell = attributes['loginShell']
print(oldshell, file=oldshell_file)

shutil.copy2(sorry_file, user_dir + '/.sorry')
os.chmod(user_dir + '/.sorry', 0o400)
shutil.chown(user_dir + '/.sorry', user=username, group='ocf')

print("Changing user's shell to sorry shell")

ocflib.infra.ldap.modify_ldap_entry(ocflib.account.utils.dn_for_username(username),
{'loginShell': SORRY_SHELL, 'gidNumber': grp.getgrnam('sorry').gr_gid})

http_dir = ocflib.account.utils.web_dir(username)
print('Changing permissions on httpdir to 000')
if (os.path.isdir(http_dir)):
os.chmod(http_dir, 0o000)

print('Changing permissions on home directory to 500')
os.chmod(user_dir, 0o500)

# Omitting final system check for now
with open(sorry_file, 'r') as filep:
file_content = filep.read()
# Log here
# Comment the next line until we figure out where to put SORRY_LOG
# write_log_file(staff_username, username)
send_log_mail(staff_username, username, file_content)

user_mail = get_email(username)
if user_mail:
send_sorry_mail(user_mail, username, file_content)
choice = input("Do you want this script to run 'note -u {0} reason' for you? [Y/n]".format(username))
if choice not in ['n', 'N']:
subprocess.run(['sudo', '-u', staff_username, 'note', '-u', format(username), file_content])
else:
print('NOW RUN note -u {0} reason. NOW'.format(username))

if kerberos_new_ticket:
kerberos_destroy()

def main():
sorry_description = textwrap.dedent('''
Sorry a user.
Standard sorry reasons are:
{0}
Nevertheless, you can also pass in your own file with its file name as argument sorry_reason.
'''.format('\n'.join([f for f in os.listdir(REASON_PATH) if os.path.isfile(os.path.join(REASON_PATH, f))])))
parser = argparse.ArgumentParser(usage='%(prog)s sorried_user sorry_reason',
formatter_class=argparse.RawDescriptionHelpFormatter,
description=sorry_description)
parser.add_argument('sorried_user', metavar='sorried_user', help='Username of sorried user')
parser.add_argument('sorry_reason', metavar='sorried_reason', help='Reason for the user to be sorried')

if os.getuid() != 0:
raise PermissionError('You must be root to run this.')

args = parser.parse_args()
file_path = find_sorry_file(args.sorry_reason)
print('Sorry file {}.'.format(file_path))

sorry_user(args.sorried_user, file_path)

if __name__ == '__main__':
sys.exit(main())