#!/usr/bin/env python

# This script sets up a test directory for testing filesystem permissions
# enforcement in UDSS such as virtual machines and containers. It must be run
# as root. For example:
#
#   $ sudo ./make-perms-test /data $USER nobody
#   $ ./fs_perms.py /data/perms_test/pass 2>&1 | egrep -v 'ok$'
#   d /data/perms_test/pass/ld.out-a~--- --- rwt mismatch
#   d /data/perms_test/pass/ld.out-r~--- --- rwt mismatch
#   f /data/perms_test/pass/lf.out-a~--- --- rw- mismatch
#   f /data/perms_test/pass/lf.out-r~--- --- rw- mismatch
#   RISK	4 mismatches in 1 directories
#
# In this case, there will be four mismatches because the symlinks are
# expected to be invalid after the pass directory is attached to the UDSS.
#
# Roughly 3,000 permission settings are evaluated in order to check files and
# directories against user, primary group, and supplemental group access.
#
# For files, we test read and write. For directories, read, write, and
# traverse. Files are not tested for execute because it's a more complicated
# test (new process needed) and if readable, someone could simply make their
# own executable copy.
#
# Compatibility: As of February 2016, this needs to be compatible with Python
# 2.6 because that's the highest version that comes with RHEL 6. We're also
# aiming to be source-compatible with Python 3.4+, but that's untested.
#
# Help: http://python-future.org/compatible_idioms.html

from __future__ import division, print_function, unicode_literals

import grp
import os
import os.path
import pwd
import sys

if (len(sys.argv) != 4):
   print('usage error (PEBKAC)', file=sys.stderr)
   sys.exit(1)

FILE_PERMS = set([0,    2,    4,    6])
DIR_PERMS =  set([0, 1, 2, 3, 4, 5, 6, 7])
ALL_PERMS = FILE_PERMS | DIR_PERMS
FILE_CONTENT = 'gary' * 19 + '\n'

testdir = os.path.abspath(sys.argv[1] + '/perms_test')
my_user = sys.argv[2]
yr_user = sys.argv[3]

me = pwd.getpwnam(my_user)
you = pwd.getpwnam(yr_user)

my_uid = me.pw_uid
my_gid = me.pw_gid
my_group = grp.getgrgid(my_gid).gr_name
yr_uid = you.pw_uid
yr_gid = you.pw_gid
yr_group = grp.getgrgid(yr_gid).gr_name

# find an arbitrary supplemental group for my_user
my_group2 = None
my_gid2 = None
for g in grp.getgrall():
   if (my_user in g.gr_mem and g.gr_name != my_group):
      my_group2 = g.gr_name
      my_gid2 = g.gr_gid
      break
if (my_group2 is None):
   print("couldn't find supplementary group for %s" % my_user, file=sys.stderr)
   sys.exit(1)
if (my_gid == yr_gid or my_gid == my_gid2):
   print('%s and %s share a group' % (my_user, yr_user), file=sys.stderr)
   sys.exit(1)


print('''\
test directory:     %(testdir)s
me:                 %(my_user)s %(my_uid)d
you:                %(yr_user)s %(yr_uid)d
my primary group:   %(my_group)s %(my_gid)d
my supp. group:     %(my_group2)s %(my_gid2)d
your primary group: %(yr_group)s %(yr_gid)d
''' % locals())


def set_perms(name, uid, gid, mode):
   os.chown(name, uid, gid)
   os.chmod(name, mode)

def symlink(src, link_name):
   if (not os.path.exists(src)):
      print('link target does not exist: %s' % src)
      sys.exit(1)
   os.symlink(src, link_name)


class Test(object):

   def __init__(self, uid, gid, up, gp, op, name=None):
      self.uid = uid
      self.group = grp.getgrgid(gid).gr_name
      self.gid = gid
      self.user = pwd.getpwuid(uid).pw_name
      self.up = up
      self.gp = gp
      self.op = op
      self.name_override = name
      self.mode = up << 6 | gp << 3 | op

      # Which permission bits govern?
      if (self.uid == my_uid):
         self.p = self.up
      elif (self.gid in (my_gid, my_gid2)):
         self.p = self.gp
      else:
         self.p = self.op

   @property
   def name(self):
      if (self.name_override is not None):
         return self.name_override
      else:
         return ('%s.%s-%s.%03o~%s'
                 % (self.type_, self.user, self.group, self.mode, self.expect))

   @property
   def valid(self):
      return (all(x in self.valid_perms for x in (self.up, self.gp, self.op)))

   def write(self):
      if (not self.valid):
         return 0
      self.write_real()
      set_perms(self.name, self.uid, self.gid, self.mode)
      return 1


class Test_Directory(Test):

   type_ = 'd'
   valid_perms = DIR_PERMS

   @property
   def expect(self):
      return (  ('r' if (self.p & 4) else '-')
              + ('w' if (self.p & 3 == 3) else '-')
              + ('t' if (self.p & 1) else '-'))

   def write_real(self):
      os.mkdir(self.name)
      # Create a file R/W by me, for testing traversal.
      file_ = self.name + '/file'
      with open(file_, 'w') as fp:
         fp.write(FILE_CONTENT)
      set_perms(file_, my_uid, my_uid, 0660)


class Test_File(Test):

   type_ = 'f'
   valid_perms = FILE_PERMS

   @property
   def expect(self):
      return (  ('r' if (self.p & 4) else '-')
              + ('w' if (self.p & 2) else '-')
              + '-')

   def write_real(self):
      with open(self.name, 'w') as fp:
         fp.write(FILE_CONTENT)


try:
   os.mkdir(testdir)
except OSError as x:
   print("can't mkdir %s: %s" % (testdir, str(x)))
   sys.exit(1)
set_perms(testdir, my_uid, my_gid, 0770)
os.chdir(testdir)

Test_Directory(my_uid, my_gid, 7, 7, 0, 'nopass').write()
os.chdir('nopass')
Test_Directory(my_uid, my_gid, 7, 7, 0, 'dir').write()
Test_File(my_uid, my_gid, 6, 6, 0, 'file').write()
os.chdir('..')

Test_Directory(my_uid, my_gid, 7, 7, 0, 'pass').write()
os.chdir('pass')
ct = 0
for uid in (my_uid, yr_uid):
   for gid in (my_gid, my_gid2, yr_gid):
      if (uid == my_uid and gid == my_gid):
         # Files owned by my_uid:my_gid are not a meaningful access control
         # test; check the documentation for why.
         continue
      for up in ALL_PERMS:
         for gp in ALL_PERMS:
            for op in ALL_PERMS:
               f = Test_File(uid, gid, up, gp, op)
               #print(f.name)
               ct += f.write()
               d = Test_Directory(uid, gid, up, gp, op)
               #print(d.name)
               ct += d.write()
               #print(ct)

symlink('f.%s-%s.600~rw-' % (my_user, yr_group), 'lf.in~rw-')
symlink('d.%s-%s.700~rwt' % (my_user, yr_group), 'ld.in~rwt')
symlink('%s/nopass/file' % testdir, 'lf.out-a~---')
symlink('%s/nopass/dir' % testdir, 'ld.out-a~---')
symlink('../nopass/file', 'lf.out-r~---')
symlink('../nopass/dir', 'ld.out-r~---')

print("created %d files and directories" % ct)
