From 59985de3b71491968218da6473251472c07b64d9 Mon Sep 17 00:00:00 2001
From: Egor Tensin <Egor.Tensin@gmail.com>
Date: Sun, 13 May 2018 21:21:36 +0300
Subject: initial commit

---
 LICENSE.txt      |  21 +++++
 writable_dirs.py | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 293 insertions(+)
 create mode 100644 LICENSE.txt
 create mode 100755 writable_dirs.py

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()
-- 
cgit v1.2.3