permissions.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. __package__ = 'archivebox.config'
  2. import os
  3. import pwd
  4. import sys
  5. import socket
  6. import platform
  7. from rich import print
  8. from pathlib import Path
  9. from contextlib import contextmanager
  10. #############################################################################################
  11. DATA_DIR = Path(os.getcwd())
  12. try:
  13. DATA_DIR_STAT = DATA_DIR.stat()
  14. DATA_DIR_UID = DATA_DIR_STAT.st_uid
  15. DATA_DIR_GID = DATA_DIR_STAT.st_gid
  16. except PermissionError:
  17. DATA_DIR_UID = 0
  18. DATA_DIR_GID = 0
  19. DEFAULT_PUID = 911
  20. DEFAULT_PGID = 911
  21. RUNNING_AS_UID = os.getuid()
  22. RUNNING_AS_GID = os.getgid()
  23. EUID = os.geteuid()
  24. EGID = os.getegid()
  25. SUDO_UID = int(os.environ.get('SUDO_UID', 0))
  26. SUDO_GID = int(os.environ.get('SUDO_GID', 0))
  27. USER: str = Path('~').expanduser().resolve().name
  28. HOSTNAME: str = max([socket.gethostname(), platform.node()], key=len)
  29. IS_ROOT = RUNNING_AS_UID == 0
  30. IN_DOCKER = os.environ.get('IN_DOCKER', False) in ('1', 'true', 'True', 'TRUE', 'yes')
  31. # IN_DOCKER_COMPOSE = # TODO: figure out a way to detect if running in docker compose
  32. FALLBACK_UID = RUNNING_AS_UID or SUDO_UID
  33. FALLBACK_GID = RUNNING_AS_GID or SUDO_GID
  34. if RUNNING_AS_UID == 0:
  35. try:
  36. # if we are running as root it's really hard to figure out what the correct archivebox user should be
  37. # as a last resort instead of setting DATA_DIR ownership to 0:0 (which breaks it for non-root users)
  38. # check if 911:911 archivebox user exists on host system, and use it instead of 0
  39. import pwd
  40. if pwd.getpwuid(DEFAULT_PUID).pw_name == 'archivebox':
  41. FALLBACK_UID = DEFAULT_PUID
  42. FALLBACK_GID = DEFAULT_PGID
  43. except Exception:
  44. pass
  45. os.environ.setdefault('PUID', str(DATA_DIR_UID or EUID or RUNNING_AS_UID or FALLBACK_UID))
  46. os.environ.setdefault('PGID', str(DATA_DIR_GID or EGID or RUNNING_AS_GID or FALLBACK_GID))
  47. ARCHIVEBOX_USER = int(os.environ['PUID'])
  48. ARCHIVEBOX_GROUP = int(os.environ['PGID'])
  49. if not USER:
  50. try:
  51. # alternative method 1 to get username
  52. USER = pwd.getpwuid(ARCHIVEBOX_USER).pw_name
  53. except Exception:
  54. pass
  55. if not USER:
  56. try:
  57. # alternative method 2 to get username
  58. import getpass
  59. USER = getpass.getuser()
  60. except Exception:
  61. pass
  62. if not USER:
  63. try:
  64. # alternative method 3 to get username
  65. USER = os.getlogin() or 'archivebox'
  66. except Exception:
  67. USER = 'archivebox'
  68. ARCHIVEBOX_USER_EXISTS = False
  69. try:
  70. pwd.getpwuid(ARCHIVEBOX_USER)
  71. ARCHIVEBOX_USER_EXISTS = True
  72. except Exception:
  73. ARCHIVEBOX_USER_EXISTS = False
  74. #############################################################################################
  75. def drop_privileges():
  76. """If running as root, drop privileges to the user that owns the data dir (or PUID)"""
  77. # always run archivebox as the user that owns the data dir, never as root
  78. if os.getuid() == 0:
  79. # drop permissions to the user that owns the data dir / provided PUID
  80. if os.geteuid() != ARCHIVEBOX_USER and ARCHIVEBOX_USER != 0 and ARCHIVEBOX_USER_EXISTS:
  81. # drop our effective UID to the archivebox user's UID
  82. os.seteuid(ARCHIVEBOX_USER)
  83. # update environment variables so that subprocesses dont try to write to /root
  84. pw_record = pwd.getpwuid(ARCHIVEBOX_USER)
  85. os.environ['HOME'] = pw_record.pw_dir
  86. os.environ['LOGNAME'] = pw_record.pw_name
  87. os.environ['USER'] = pw_record.pw_name
  88. if ARCHIVEBOX_USER == 0 or not ARCHIVEBOX_USER_EXISTS:
  89. print('[yellow]:warning: Running as [red]root[/red] is not recommended and may make your [blue]DATA_DIR[/blue] inaccessible to other users on your system.[/yellow] (use [blue]sudo[/blue] instead)', file=sys.stderr)
  90. @contextmanager
  91. def SudoPermission(uid=0, fallback=False):
  92. """Attempt to run code with sudo permissions for a given user (or root)"""
  93. if os.geteuid() == uid:
  94. # no need to change effective UID, we are already that user
  95. yield
  96. return
  97. try:
  98. # change our effective UID to the given UID
  99. os.seteuid(uid)
  100. except PermissionError as err:
  101. if not fallback:
  102. raise PermissionError(f'Not enough permissions to run code as uid={uid}, please retry with sudo') from err
  103. try:
  104. # yield back to the caller so they can run code inside context as root
  105. yield
  106. finally:
  107. # then set effective UID back to DATA_DIR owner
  108. try:
  109. os.seteuid(ARCHIVEBOX_USER)
  110. except PermissionError as err:
  111. if not fallback:
  112. raise PermissionError(f'Failed to revert uid={uid} back to {ARCHIVEBOX_USER} after running code with sudo') from err