#!/usr/bin/python3
"""Devuelve a cada usuario la propiedad de ~/.local, ~/.local/share,
~/.local/share/applications y ~/.config cuando versiones anteriores de obs-quirinux y otras herramientas los
dejaron con propietario root (los usuarios no podian escribir en ~/.local/share).

Seguridad: root opera aqui sobre directorios que controla el usuario. Para que un
enlace simbolico (o un cambio de ruta a mitad de ejecucion) no pueda desviar el
chown hacia ficheros del sistema, el arbol se recorre con descriptores de fichero:
cada componente se abre con O_NOFOLLOW|O_DIRECTORY relativo al anterior, se verifica
con fstat() sobre el propio descriptor y se cambia con fchown() sobre ese mismo
descriptor. Solo se tocan directorios cuyo propietario es root, dentro de un home
que pertenece al usuario y en el mismo sistema de ficheros.

Uso: fix-local-dirs [usuario ...]     (sin argumentos: todos los usuarios con UID>=1000)
Nunca falla (codigo de salida 0) y no hace preguntas.
"""
import os
import pwd
import stat
import subprocess
import sys

OPEN = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC
CHAINS = (('.local', 'share', 'applications'), ('.config',))


def fix_user(pw):
    """Devuelve (lista de rutas corregidas, mensaje de error o None)."""
    fixed = []
    fds = []
    try:
        try:
            hfd = os.open(pw.pw_dir, OPEN)
        except OSError:
            return fixed, None            # sin directorio personal: nada que hacer
        fds.append(hfd)
        hst = os.fstat(hfd)
        if hst.st_uid != pw.pw_uid:
            return fixed, None            # el home no es del usuario: no se toca
        for chain in CHAINS:
            cur = hfd
            rel = ''
            for name in chain:
                try:
                    fd = os.open(name, OPEN | os.O_NOFOLLOW, dir_fd=cur)
                except OSError:
                    break                 # no existe, es un enlace o no es un directorio
                fds.append(fd)
                st = os.fstat(fd)
                if st.st_dev != hst.st_dev:
                    break                 # otro sistema de ficheros (montaje)
                rel += '/' + name
                if st.st_uid == 0:
                    gid = pw.pw_gid if st.st_gid == 0 else -1
                    os.fchown(fd, pw.pw_uid, gid)
                    if (st.st_mode & 0o700) != 0o700:
                        os.fchmod(fd, stat.S_IMODE(st.st_mode) | 0o700)
                    fixed.append(rel)
                cur = fd
    except OSError as exc:
        return fixed, str(exc)
    finally:
        for fd in fds:
            os.close(fd)
    return fixed, None


def writable_by_user(pw, path):
    try:
        r = subprocess.run(['runuser', '-u', pw.pw_name, '--', 'test', '-w', path],
                           stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL, timeout=20)
        return r.returncode == 0
    except (OSError, subprocess.SubprocessError):
        return True                       # no se puede comprobar: no alarmar


def main(argv):
    only = set(argv[1:])
    users = nfixed = 0
    for pw in pwd.getpwall():
        if pw.pw_uid < 1000 or pw.pw_name == 'nobody':
            continue
        if only and pw.pw_name not in only:
            continue
        fixed, err = fix_user(pw)
        if fixed:
            users += 1
            nfixed += len(fixed)
            print('quirinux-config: %s: propietario corregido en %s'
                  % (pw.pw_name, ', '.join('~' + p for p in fixed)))
        if err:
            print('quirinux-config: aviso: %s: %s' % (pw.pw_name, err), file=sys.stderr)
        try:
            if os.stat(pw.pw_dir).st_uid != pw.pw_uid:
                continue                  # home ajeno: no se evalua
        except OSError:
            continue
        for sub in (('.local', 'share'), ('.config',)):
            d = os.path.join(pw.pw_dir, *sub)
            if os.path.isdir(d) and not os.path.islink(d) and not writable_by_user(pw, d):
                print('quirinux-config: AVISO: %s no puede escribir en %s' % (pw.pw_name, d),
                      file=sys.stderr)
    if nfixed:
        print('quirinux-config: %d directorios corregidos en %d usuarios' % (nfixed, users))
    return 0


if __name__ == '__main__':
    try:
        sys.exit(main(sys.argv))
    except Exception as exc:              # nunca romper una instalacion
        print('quirinux-config: aviso: %s' % exc, file=sys.stderr)
        sys.exit(0)
