deps_perms.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import json
  2. import pathlib
  3. from typing import TYPE_CHECKING
  4. if TYPE_CHECKING:
  5. from render import Render
  6. from storage import IxStorage
  7. try:
  8. from .error import RenderError
  9. from .validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
  10. except ImportError:
  11. from error import RenderError
  12. from validations import valid_octal_mode_or_raise, valid_fs_path_or_raise
  13. class PermsContainer:
  14. def __init__(self, render_instance: "Render", name: str):
  15. self._render_instance = render_instance
  16. self._name = name
  17. self.actions: set[str] = set()
  18. self.parsed_configs: list[dict] = []
  19. def add_or_skip_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
  20. identifier = self.normalize_identifier_for_path(identifier)
  21. if identifier in self.actions:
  22. raise RenderError(f"Action with id [{identifier}] already used for another permission action")
  23. parsed_action = self.parse_action(identifier, volume_config, action_config)
  24. if parsed_action:
  25. self.parsed_configs.append(parsed_action)
  26. self.actions.add(identifier)
  27. def parse_action(self, identifier: str, volume_config: "IxStorage", action_config: dict):
  28. valid_modes = [
  29. "always", # Always set permissions, without checking.
  30. "check", # Checks if permissions are correct, and set them if not.
  31. ]
  32. mode = action_config.get("mode", "check")
  33. uid = action_config.get("uid", None)
  34. gid = action_config.get("gid", None)
  35. chmod = action_config.get("chmod", None)
  36. recursive = action_config.get("recursive", False)
  37. mount_path = pathlib.Path("/mnt/permission", identifier).as_posix()
  38. read_only = volume_config.get("read_only", False)
  39. is_temporary = False
  40. vol_type = volume_config.get("type", "")
  41. match vol_type:
  42. case "temporary":
  43. # If it is a temporary volume, we force auto permissions
  44. # and set is_temporary to True, so it will be cleaned up
  45. is_temporary = True
  46. recursive = True
  47. case "volume":
  48. if not volume_config.get("volume_config", {}).get("auto_permissions", False):
  49. return None
  50. case "host_path":
  51. host_path_config = volume_config.get("host_path_config", {})
  52. # Skip when ACL enabled
  53. if host_path_config.get("acl_enable", False):
  54. return None
  55. if not host_path_config.get("auto_permissions", False):
  56. return None
  57. case "ix_volume":
  58. ix_vol_config = volume_config.get("ix_volume_config", {})
  59. # Skip when ACL enabled
  60. if ix_vol_config.get("acl_enable", False):
  61. return None
  62. # For ix_volumes, we default to auto_permissions = True
  63. if not ix_vol_config.get("auto_permissions", True):
  64. return None
  65. case _:
  66. # Skip for other types
  67. return None
  68. if mode not in valid_modes:
  69. raise RenderError(f"Expected [mode] to be one of [{', '.join(valid_modes)}], got [{mode}]")
  70. if not isinstance(uid, int) or not isinstance(gid, int):
  71. raise RenderError("Expected [uid] and [gid] to be set when [auto_permissions] is enabled")
  72. if chmod is not None:
  73. chmod = valid_octal_mode_or_raise(chmod)
  74. mount_path = valid_fs_path_or_raise(mount_path)
  75. return {
  76. "mount_path": mount_path,
  77. "volume_config": volume_config,
  78. "action_data": {
  79. "read_only": read_only,
  80. "mount_path": mount_path,
  81. "is_temporary": is_temporary,
  82. "identifier": identifier,
  83. "recursive": recursive,
  84. "mode": mode,
  85. "uid": uid,
  86. "gid": gid,
  87. "chmod": chmod,
  88. },
  89. }
  90. def normalize_identifier_for_path(self, identifier: str):
  91. return identifier.rstrip("/").lstrip("/").lower().replace("/", "_").replace(".", "-").replace(" ", "-")
  92. def has_actions(self):
  93. return bool(self.actions)
  94. def activate(self):
  95. if len(self.parsed_configs) != len(self.actions):
  96. raise RenderError("Number of actions and parsed configs does not match")
  97. if not self.has_actions():
  98. raise RenderError("No actions added. Check if there are actions before activating")
  99. # Add the container and set it up
  100. c = self._render_instance.add_container(self._name, "python_permissions_image")
  101. c.set_user(0, 0)
  102. c.add_caps(["CHOWN", "FOWNER", "DAC_OVERRIDE"])
  103. c.set_network_mode("none")
  104. # Don't attach any devices
  105. c.remove_devices()
  106. c.deploy.resources.set_profile("medium")
  107. c.restart.set_policy("on-failure", maximum_retry_count=1)
  108. c.healthcheck.disable()
  109. c.set_entrypoint(["python3", "/script/run.py"])
  110. script = "#!/usr/bin/env python3\n"
  111. script += get_script()
  112. c.configs.add("permissions_run_script", script, "/script/run.py", "0700")
  113. actions_data: list[dict] = []
  114. for parsed in self.parsed_configs:
  115. if not parsed["action_data"]["read_only"]:
  116. c.add_storage(parsed["mount_path"], parsed["volume_config"])
  117. actions_data.append(parsed["action_data"])
  118. actions_data_json = json.dumps(actions_data)
  119. c.configs.add("permissions_actions_data", actions_data_json, "/script/actions.json", "0500")
  120. def get_script():
  121. return """
  122. import os
  123. import json
  124. import time
  125. import shutil
  126. with open("/script/actions.json", "r") as f:
  127. actions_data = json.load(f)
  128. if not actions_data:
  129. # If this script is called, there should be actions data
  130. raise ValueError("No actions data found")
  131. def fix_perms(path, chmod, recursive=False):
  132. print(f"Changing permissions{' recursively ' if recursive else ' '}to {chmod} on: [{path}]")
  133. os.chmod(path, int(chmod, 8))
  134. if recursive:
  135. for root, dirs, files in os.walk(path):
  136. for f in files:
  137. os.chmod(os.path.join(root, f), int(chmod, 8))
  138. print("Permissions after changes:")
  139. print_chmod_stat()
  140. def fix_owner(path, uid, gid, recursive=False):
  141. print(f"Changing ownership{' recursively ' if recursive else ' '}to {uid}:{gid} on: [{path}]")
  142. os.chown(path, uid, gid)
  143. if recursive:
  144. for root, dirs, files in os.walk(path):
  145. for f in files:
  146. os.chown(os.path.join(root, f), uid, gid)
  147. print("Ownership after changes:")
  148. print_chown_stat()
  149. def print_chown_stat():
  150. curr_stat = os.stat(action["mount_path"])
  151. print(f"Ownership: [{curr_stat.st_uid}:{curr_stat.st_gid}]")
  152. def print_chmod_stat():
  153. curr_stat = os.stat(action["mount_path"])
  154. print(f"Permissions: [{oct(curr_stat.st_mode)[3:]}]")
  155. def print_chown_diff(curr_stat, uid, gid):
  156. print(f"Ownership: wanted [{uid}:{gid}], got [{curr_stat.st_uid}:{curr_stat.st_gid}].")
  157. def print_chmod_diff(curr_stat, mode):
  158. print(f"Permissions: wanted [{mode}], got [{oct(curr_stat.st_mode)[3:]}].")
  159. def perform_action(action):
  160. if action["read_only"]:
  161. print(f"Path for action [{action['identifier']}] is read-only, skipping...")
  162. return
  163. start_time = time.time()
  164. print(f"=== Applying configuration on volume with identifier [{action['identifier']}] ===")
  165. if not os.path.isdir(action["mount_path"]):
  166. print(f"Path [{action['mount_path']}] is not a directory, skipping...")
  167. return
  168. if action["is_temporary"]:
  169. print(f"Path [{action['mount_path']}] is a temporary directory, ensuring it is empty...")
  170. for item in os.listdir(action["mount_path"]):
  171. item_path = os.path.join(action["mount_path"], item)
  172. # Exclude the safe directory, where we can use to mount files temporarily
  173. if os.path.basename(item_path) == "ix-safe":
  174. continue
  175. if os.path.isdir(item_path):
  176. shutil.rmtree(item_path)
  177. else:
  178. os.remove(item_path)
  179. if not action["is_temporary"] and os.listdir(action["mount_path"]):
  180. print(f"Path [{action['mount_path']}] is not empty, skipping...")
  181. return
  182. print(f"Current Ownership and Permissions on [{action['mount_path']}]:")
  183. curr_stat = os.stat(action["mount_path"])
  184. print_chown_diff(curr_stat, action["uid"], action["gid"])
  185. print_chmod_diff(curr_stat, action["chmod"])
  186. print("---")
  187. if action["mode"] == "always":
  188. fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
  189. if not action["chmod"]:
  190. print("Skipping permissions check, chmod is falsy")
  191. else:
  192. fix_perms(action["mount_path"], action["chmod"], action["recursive"])
  193. return
  194. elif action["mode"] == "check":
  195. if curr_stat.st_uid != action["uid"] or curr_stat.st_gid != action["gid"]:
  196. print("Ownership is incorrect. Fixing...")
  197. fix_owner(action["mount_path"], action["uid"], action["gid"], action["recursive"])
  198. else:
  199. print("Ownership is correct. Skipping...")
  200. if not action["chmod"]:
  201. print("Skipping permissions check, chmod is falsy")
  202. else:
  203. if oct(curr_stat.st_mode)[3:] != action["chmod"]:
  204. print("Permissions are incorrect. Fixing...")
  205. fix_perms(action["mount_path"], action["chmod"], action["recursive"])
  206. else:
  207. print("Permissions are correct. Skipping...")
  208. print(f"Time taken: {(time.time() - start_time) * 1000:.2f}ms")
  209. print(f"=== Finished applying configuration on volume with identifier [{action['identifier']}] ==")
  210. print()
  211. if __name__ == "__main__":
  212. start_time = time.time()
  213. for action in actions_data:
  214. perform_action(action)
  215. print(f"Total time taken: {(time.time() - start_time) * 1000:.2f}ms")
  216. """