#!/usr/bin/python3
"""Devuelve a cada usuario la propiedad de ~/.local, ~/.local/share y
~/.local/share/applications cuando versiones anteriores de obs-quirinux 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
CHAIN = ('.local', 'share', 'applications')


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
        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('obs-quirinux: %s: propietario corregido en %s'
                  % (pw.pw_name, ', '.join('~' + p for p in fixed)))
        if err:
            print('obs-quirinux: 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
        share = os.path.join(pw.pw_dir, '.local', 'share')
        if os.path.isdir(share) and not os.path.islink(share) \
                and not writable_by_user(pw, share):
            print('obs-quirinux: AVISO: %s no puede escribir en %s' % (pw.pw_name, share),
                  file=sys.stderr)
    if nfixed:
        print('obs-quirinux: %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('obs-quirinux: aviso: %s' % exc, file=sys.stderr)
        sys.exit(0)
