aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--LICENSE.txt21
-rwxr-xr-xwritable_dirs.py272
2 files changed, 293 insertions, 0 deletions
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..977fc1d
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Egor Tensin <Egor.Tensin@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/writable_dirs.py b/writable_dirs.py
new file mode 100755
index 0000000..39a0f40
--- /dev/null
+++ b/writable_dirs.py
@@ -0,0 +1,272 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2018 Egor Tensin <Egor.Tensin@gmail.com>
+# This file is part of the "writable-dirs" project.
+# For details, see https://github.com/egor-tensin/writable-dirs.
+# Distributed under the MIT License.
+
+import argparse
+import contextlib
+import grp
+import logging
+import logging.config
+import logging.handlers
+import multiprocessing as mp
+import os
+import pwd
+import sys
+
+
+def console_log_formatter():
+ fmt = '%(asctime)s | %(process)d | %(levelname)s | %(message)s'
+ datefmt = '%Y-%m-%d %H:%M:%S'
+ return logging.Formatter(fmt=fmt, datefmt=datefmt)
+
+
+def console_log_level():
+ return logging.DEBUG
+
+
+def console_log_handler():
+ handler = logging.StreamHandler()
+ handler.setFormatter(console_log_formatter())
+ handler.setLevel(console_log_level())
+ return handler
+
+
+@contextlib.contextmanager
+def launch_logging_thread(queue):
+ listener = logging.handlers.QueueListener(queue, console_log_handler())
+ listener.start()
+ try:
+ yield
+ finally:
+ listener.stop()
+
+
+@contextlib.contextmanager
+def set_up_logging(queue):
+ config = {
+ 'version': 1,
+ 'handlers': {
+ 'sink': {
+ 'class': 'logging.handlers.QueueHandler',
+ 'queue': queue,
+ },
+ },
+ 'root': {
+ 'handlers': ['sink'],
+ 'level': 'DEBUG',
+ },
+ }
+ logging.config.dictConfig(config)
+ try:
+ yield
+ except Exception as e:
+ logging.exception(e)
+
+
+def validate_uid(uid):
+ try:
+ return pwd.getpwuid(uid).pw_uid
+ except KeyError:
+ return None
+
+
+def validate_gid(gid):
+ try:
+ return grp.getgrgid(gid).gr_gid
+ except KeyError:
+ return None
+
+
+def map_user_name(user_name):
+ try:
+ return pwd.getpwnam(user_name).pw_uid
+ except KeyError:
+ return None
+
+
+def map_group_name(group_name):
+ try:
+ return grp.getgrnam(group_name).gr_gid
+ except KeyError:
+ return None
+
+
+def parse_user_name(src):
+ uid = map_user_name(src)
+ if uid is None:
+ raise argparse.ArgumentTypeError('unknown user name: {}'.format(src))
+ return uid
+
+
+def parse_group_name(src):
+ gid = map_group_name(src)
+ if gid is None:
+ raise argparse.ArgumentTypeError('unknown group name: {}'.format(src))
+ return gid
+
+
+def parse_uid(src):
+ try:
+ uid = int(src)
+ except ValueError:
+ return parse_user_name(src)
+ uid = validate_uid(uid)
+ if uid is None:
+ raise argparse.ArgumentTypeError('unknown UID: {}'.format(src))
+ return uid
+
+
+def parse_gid(src):
+ try:
+ gid = int(src)
+ except ValueError:
+ return parse_group_name(src)
+ gid = validate_gid(gid)
+ if gid is None:
+ raise argparse.ArgumentTypeError('unknown GID: {}'.format(src))
+ return gid
+
+
+def get_primary_gid(uid):
+ return pwd.getpwuid(uid).pw_gid
+
+
+def parse_args(argv=None):
+ if argv is None:
+ argv = sys.argv[1:]
+ parser = argparse.ArgumentParser()
+ parser.add_argument('root_dir', default='/', nargs='?', metavar='DIR',
+ help='set root directory')
+ parser.add_argument('--user', '-u', dest='uid', required=True,
+ metavar='USER', type=parse_uid,
+ help='set new process\' UID')
+ parser.add_argument('--group', '-g', dest='gid',
+ metavar='GROUP', type=parse_gid,
+ help='set new process\' GID')
+ args = parser.parse_args(argv)
+ if args.gid is None:
+ args.gid = get_primary_gid(args.uid)
+ return args
+
+
+def dump_process_info():
+ ruid, euid, suid = os.getresuid()
+ logging.info('User IDs:')
+ logging.info('\tReal: %d', ruid)
+ logging.info('\tEffective: %d', euid)
+ logging.info('\tSaved: %d', suid)
+ rgid, egid, sgid = os.getresgid()
+ logging.info('Group IDs:')
+ logging.info('\tReal: %d', rgid)
+ logging.info('\tEffective: %d', egid)
+ logging.info('\tSaved: %d', sgid)
+
+
+def check_root():
+ if os.getuid() == 0:
+ return True
+ logging.error('Must be run as root')
+ return False
+
+
+def scandir(dir_path):
+ try:
+ entry_it = os.scandir(dir_path)
+ except (PermissionError, FileNotFoundError) as e:
+ logging.warning('%s', e)
+ return
+ # On Python 3.6+:
+ # with entry_it:
+ yield from entry_it
+
+
+def enum_dirs(dir_path):
+ for entry in scandir(dir_path):
+ if entry.is_dir(follow_symlinks=False):
+ yield entry.path
+
+
+def is_writable_via_access(dir_path):
+ return os.access(dir_path, os.W_OK | os.X_OK)
+
+
+def is_writable(dir_path):
+ return is_writable_via_access(dir_path)
+
+
+def drop_privileges(uid, gid):
+ if not check_root():
+ return
+ os.setgroups([])
+ os.setgid(gid)
+ os.setuid(uid)
+ os.umask(0o77)
+
+
+@contextlib.contextmanager
+def close_queue(queue):
+ try:
+ yield
+ finally:
+ queue.put(None)
+
+
+def access_loop(access_queue, scandir_queue):
+ for dir_list in iter(access_queue.get, None):
+ denied_dir_list = []
+ for dir_path in dir_list:
+ if is_writable(dir_path):
+ logging.info('Writable: %s', dir_path)
+ else:
+ denied_dir_list.append(dir_path)
+ if not denied_dir_list:
+ break
+ scandir_queue.put(denied_dir_list)
+
+
+def access_main(args, log_queue, access_queue, scandir_queue):
+ with close_queue(scandir_queue), set_up_logging(log_queue):
+ drop_privileges(args.uid, args.gid)
+ dump_process_info()
+ access_loop(access_queue, scandir_queue)
+
+
+def scandir_loop(access_queue, scandir_queue):
+ for parent_dir_list in iter(scandir_queue.get, None):
+ child_dir_list = [child_dir
+ for parent_dir in parent_dir_list
+ for child_dir in enum_dirs(parent_dir)]
+ if not child_dir_list:
+ break
+ access_queue.put(child_dir_list)
+
+
+def scandir_main(access_queue, scandir_queue):
+ with close_queue(access_queue):
+ if not check_root():
+ return
+ dump_process_info()
+ scandir_loop(access_queue, scandir_queue)
+
+
+def main(argv=None):
+ log_queue = mp.Queue()
+ with launch_logging_thread(log_queue), set_up_logging(log_queue):
+ prog_args = parse_args(argv)
+ access_queue = mp.SimpleQueue()
+ scandir_queue = mp.SimpleQueue()
+ access_process_args = prog_args, log_queue, access_queue, scandir_queue
+ access_process = mp.Process(target=access_main,
+ args=access_process_args)
+ access_queue.put([prog_args.root_dir])
+ access_process.start()
+ scandir_main(access_queue, scandir_queue)
+ access_process.join()
+
+
+if __name__ == '__main__':
+ mp.set_start_method('spawn')
+ main()