#!/usr/bin/env python3
#
# from here http://tbrink.science/blog/2017/06/20/converting-privileged-lxc-containers-to-unprivileged-containers/
# Written 2017 by Tobias Brink
#
# To the extent possible under law, the author(s) have dedicated
# all copyright and related and neighboring rights to this software
# to the public domain worldwide. This software is distributed
# without any warranty.
#
# You should have received a copy of the CC0 Public Domain
# Dedication along with this software. If not, see
# <http://creativecommons.org/publicdomain/zero/1.0/>.

import sys
import os

try:
    import posix1e # "pylibacl" package
except ImportError:
    print("WARNING: pylibacl missing, cannot update ACLs!")
    has_posix1e = False
else:
    has_posix1e = True

# Call the script with two arguments:
#   1) the root directory of the container, e.g.,
#      /var/lib/lxc/foo/rootfs/
#   2) the UID/GID offset, in the example case from
#      above that would be 10000000
directory = sys.argv[1]
offset = int(sys.argv[2])

# Preview action and require confirmation.
print("directory     :", directory)
print("UID/GID offset:", offset)
print()
ans = input("Is this correct? [y/N] ")
if ans.lower() != "y":
    sys.exit()


# This functions shifts either an access or a default ACL. This is
# determined by the "flag" parameter.
def shift_acl(fp, offset, flag):
    """Shift either access or default ACL by offset."""
    # What kind of ACL do we have?
    acl = (posix1e.ACL(file=fp)
           if flag == posix1e.ACL_TYPE_ACCESS
           else posix1e.ACL(filedef=fp))
    # See if we need to update ACL and add offsets.
    acl_needs_update = False
    for entry in acl:
        if entry.tag_type in {posix1e.ACL_USER,
                              posix1e.ACL_GROUP}:
            acl_needs_update = True
            entry.qualifier += offset
    if acl_needs_update:
        # Something has changed, update the file.
        acl.applyto(fp, flag)


# This one shifts the UIDs/GIDs and corrects the setuid/setgid
# bits.
def shift_ids(fp, offset, seen_inodes):
    """Shift the UIDs/GIDs of the file "fp" by "offset"."""
    # Get the current UID/GID/permissions.
    stat = os.stat(fp, follow_symlinks=False)
    # Ensure that we haven't already shifted the IDs of
    # this inode.
    if (stat.st_dev, stat.st_ino) in seen_inodes:
        # Already changed that file.
        return
    else:
        # Remember this inode to avoid shifting its
        # IDs again.
        seen_inodes.add( (stat.st_dev, stat.st_ino) )
    # Add the offset.
    new_uid = stat.st_uid + offset
    new_gid = stat.st_gid + offset
    # Change the UID and GID.
    os.chown(fp, new_uid, new_gid, follow_symlinks=False)
    # Permissions are not relevant for symlinks on Linux.
    if not os.path.islink(fp):
        # Restore mode, because (e.g.) setuid may be stripped
        # by chown.
        os.chmod(fp, stat.st_mode)
        # Update ACL.
        if not has_posix1e: return
        shift_acl(fp, offset, posix1e.ACL_TYPE_ACCESS)
        if os.path.isdir(fp):
            # Directories can also have a "default ACL".
            shift_acl(fp, offset, posix1e.ACL_TYPE_DEFAULT)


# Apply transformation to the directory itself.
seen_inodes = set()
shift_ids(directory, offset, seen_inodes)
# Recursively walk through the directory tree of the container.
for root, dirs, files in os.walk(directory):
    for obj in dirs + files:
        fp = os.path.join(root, obj)
        # Modify.
        shift_ids(fp, offset, seen_inodes)
