validations.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. import re
  2. import ipaddress
  3. from pathlib import Path
  4. try:
  5. from .error import RenderError
  6. except ImportError:
  7. from error import RenderError
  8. OCTAL_MODE_REGEX = re.compile(r"^0[0-7]{3}$")
  9. RESTRICTED_IN: tuple[Path, ...] = (Path("/mnt"), Path("/"))
  10. RESTRICTED: tuple[Path, ...] = (
  11. Path("/mnt/.ix-apps"),
  12. Path("/data"),
  13. Path("/var/db"),
  14. Path("/root"),
  15. Path("/conf"),
  16. Path("/audit"),
  17. Path("/var/run/middleware"),
  18. Path("/home"),
  19. Path("/boot"),
  20. Path("/var/log"),
  21. )
  22. def valid_security_opt_or_raise(opt: str):
  23. if ":" in opt or "=" in opt:
  24. raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")
  25. valid_opts = ["apparmor", "no-new-privileges", "seccomp", "systempaths", "label"]
  26. if opt not in valid_opts:
  27. raise RenderError(f"Security Option [{opt}] is not valid. Valid options are: [{', '.join(valid_opts)}]")
  28. return opt
  29. def valid_port_bind_mode_or_raise(status: str):
  30. valid_statuses = ("published", "exposed", "")
  31. if status not in valid_statuses:
  32. raise RenderError(f"Invalid port status [{status}]")
  33. return status
  34. def valid_pull_policy_or_raise(pull_policy: str):
  35. valid_policies = ("missing", "always", "never", "build")
  36. if pull_policy not in valid_policies:
  37. raise RenderError(f"Pull policy [{pull_policy}] is not valid. Valid options are: [{', '.join(valid_policies)}]")
  38. return pull_policy
  39. def valid_ipc_mode_or_raise(ipc_mode: str, containers: list[str]):
  40. valid_modes = ("", "host", "private", "shareable", "none")
  41. if ipc_mode in valid_modes:
  42. return ipc_mode
  43. if ipc_mode.startswith("container:"):
  44. if ipc_mode[10:] not in containers:
  45. raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
  46. return ipc_mode
  47. raise RenderError(f"IPC mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
  48. def valid_pid_mode_or_raise(ipc_mode: str, containers: list[str]):
  49. valid_modes = ("", "host")
  50. if ipc_mode in valid_modes:
  51. return ipc_mode
  52. if ipc_mode.startswith("container:"):
  53. if ipc_mode[10:] not in containers:
  54. raise RenderError(f"PID mode [{ipc_mode}] is not valid. Container [{ipc_mode[10:]}] does not exist")
  55. return ipc_mode
  56. raise RenderError(f"PID mode [{ipc_mode}] is not valid. Valid options are: [{', '.join(valid_modes)}]")
  57. def valid_sysctl_or_raise(sysctl: str, host_network: bool):
  58. if not sysctl:
  59. raise RenderError("Sysctl cannot be empty")
  60. if host_network and sysctl.startswith("net."):
  61. raise RenderError(f"Sysctl [{sysctl}] cannot start with [net.] when host network is enabled")
  62. valid_sysctls = [
  63. "kernel.msgmax",
  64. "kernel.msgmnb",
  65. "kernel.msgmni",
  66. "kernel.sem",
  67. "kernel.shmall",
  68. "kernel.shmmax",
  69. "kernel.shmmni",
  70. "kernel.shm_rmid_forced",
  71. ]
  72. # https://docs.docker.com/reference/cli/docker/container/run/#currently-supported-sysctls
  73. if not sysctl.startswith("fs.mqueue.") and not sysctl.startswith("net.") and sysctl not in valid_sysctls:
  74. raise RenderError(
  75. f"Sysctl [{sysctl}] is not valid. Valid options are: [{', '.join(valid_sysctls)}], [net.*], [fs.mqueue.*]"
  76. )
  77. return sysctl
  78. def valid_redis_password_or_raise(password: str):
  79. forbidden_chars = [" ", "'", "#"]
  80. for char in forbidden_chars:
  81. if char in password:
  82. raise RenderError(f"Redis password cannot contain [{char}]")
  83. def valid_octal_mode_or_raise(mode: str):
  84. mode = str(mode)
  85. if not OCTAL_MODE_REGEX.match(mode):
  86. raise RenderError(f"Expected [mode] to be a octal string, got [{mode}]")
  87. return mode
  88. def valid_host_path_propagation(propagation: str):
  89. valid_propagations = ("shared", "slave", "private", "rshared", "rslave", "rprivate")
  90. if propagation not in valid_propagations:
  91. raise RenderError(f"Expected [propagation] to be one of [{', '.join(valid_propagations)}], got [{propagation}]")
  92. return propagation
  93. def valid_portal_scheme_or_raise(scheme: str):
  94. schemes = ("http", "https")
  95. if scheme not in schemes:
  96. raise RenderError(f"Portal Scheme [{scheme}] is not valid. Valid options are: [{', '.join(schemes)}]")
  97. return scheme
  98. def valid_port_or_raise(port: int):
  99. if port < 1 or port > 65535:
  100. raise RenderError(f"Invalid port [{port}]. Valid ports are between 1 and 65535")
  101. return port
  102. def valid_ip_or_raise(ip: str):
  103. try:
  104. ipaddress.ip_address(ip)
  105. except ValueError:
  106. raise RenderError(f"Invalid IP address [{ip}]")
  107. return ip
  108. def valid_port_mode_or_raise(mode: str):
  109. modes = ("ingress", "host")
  110. if mode not in modes:
  111. raise RenderError(f"Port Mode [{mode}] is not valid. Valid options are: [{', '.join(modes)}]")
  112. return mode
  113. def valid_port_protocol_or_raise(protocol: str):
  114. protocols = ("tcp", "udp")
  115. if protocol not in protocols:
  116. raise RenderError(f"Port Protocol [{protocol}] is not valid. Valid options are: [{', '.join(protocols)}]")
  117. return protocol
  118. def valid_depend_condition_or_raise(condition: str):
  119. valid_conditions = ("service_started", "service_healthy", "service_completed_successfully")
  120. if condition not in valid_conditions:
  121. raise RenderError(
  122. f"Depend Condition [{condition}] is not valid. Valid options are: [{', '.join(valid_conditions)}]"
  123. )
  124. return condition
  125. def valid_cgroup_perm_or_raise(cgroup_perm: str):
  126. valid_cgroup_perms = ("r", "w", "m", "rw", "rm", "wm", "rwm", "")
  127. if cgroup_perm not in valid_cgroup_perms:
  128. raise RenderError(
  129. f"Cgroup Permission [{cgroup_perm}] is not valid. Valid options are: [{', '.join(valid_cgroup_perms)}]"
  130. )
  131. return cgroup_perm
  132. def valid_cgroup_or_raise(cgroup: str):
  133. valid_cgroup = ("host", "private")
  134. if cgroup not in valid_cgroup:
  135. raise RenderError(f"Cgroup [{cgroup}] is not valid. Valid options are: [{', '.join(valid_cgroup)}]")
  136. return cgroup
  137. def valid_device_cgroup_rule_or_raise(dev_grp_rule: str):
  138. parts = dev_grp_rule.split(" ")
  139. if len(parts) != 3:
  140. raise RenderError(
  141. f"Device Group Rule [{dev_grp_rule}] is not valid. Expected format is [<type> <major>:<minor> <permission>]"
  142. )
  143. valid_types = ("a", "b", "c")
  144. if parts[0] not in valid_types:
  145. raise RenderError(
  146. f"Device Group Rule [{dev_grp_rule}] is not valid. Expected type to be one of [{', '.join(valid_types)}]"
  147. f" but got [{parts[0]}]"
  148. )
  149. major, minor = parts[1].split(":")
  150. for part in (major, minor):
  151. if part != "*" and not part.isdigit():
  152. raise RenderError(
  153. f"Device Group Rule [{dev_grp_rule}] is not valid. Expected major and minor to be digits"
  154. f" or [*] but got [{major}] and [{minor}]"
  155. )
  156. valid_cgroup_perm_or_raise(parts[2])
  157. return dev_grp_rule
  158. def allowed_dns_opt_or_raise(dns_opt: str):
  159. disallowed_dns_opts = []
  160. if dns_opt in disallowed_dns_opts:
  161. raise RenderError(f"DNS Option [{dns_opt}] is not allowed to added.")
  162. return dns_opt
  163. def valid_http_path_or_raise(path: str):
  164. path = _valid_path_or_raise(path)
  165. return path
  166. def valid_fs_path_or_raise(path: str):
  167. # There is no reason to allow / as a path,
  168. # either on host or in a container side.
  169. if path == "/":
  170. raise RenderError(f"Path [{path}] cannot be [/]")
  171. path = _valid_path_or_raise(path)
  172. return path
  173. def is_allowed_path(input_path: str, is_ix_volume: bool = False) -> bool:
  174. """
  175. Validates that the given path (after resolving symlinks) is not
  176. one of the restricted paths or within those restricted directories.
  177. Returns True if the path is allowed, False otherwise.
  178. """
  179. # Resolve the path to avoid symlink bypasses
  180. real_path = Path(input_path).resolve()
  181. for restricted in RESTRICTED if not is_ix_volume else [r for r in RESTRICTED if r != Path("/mnt/.ix-apps")]:
  182. if real_path.is_relative_to(restricted):
  183. return False
  184. return real_path not in RESTRICTED_IN
  185. def allowed_fs_host_path_or_raise(path: str, is_ix_volume: bool = False):
  186. if not is_allowed_path(path, is_ix_volume):
  187. raise RenderError(f"Path [{path}] is not allowed to be mounted.")
  188. return path
  189. def _valid_path_or_raise(path: str):
  190. if path == "":
  191. raise RenderError(f"Path [{path}] cannot be empty")
  192. if not path.startswith("/"):
  193. raise RenderError(f"Path [{path}] must start with /")
  194. if "//" in path:
  195. raise RenderError(f"Path [{path}] cannot contain [//]")
  196. return path
  197. def allowed_device_or_raise(path: str):
  198. disallowed_devices = ["/dev/dri", "/dev/kfd", "/dev/bus/usb", "/dev/snd", "/dev/net/tun"]
  199. if path in disallowed_devices:
  200. raise RenderError(f"Device [{path}] is not allowed to be manually added.")
  201. return path
  202. def valid_network_mode_or_raise(mode: str, containers: list[str]):
  203. valid_modes = ("host", "none")
  204. if mode in valid_modes:
  205. return mode
  206. if mode.startswith("service:"):
  207. if mode[8:] not in containers:
  208. raise RenderError(f"Service [{mode[8:]}] not found")
  209. return mode
  210. raise RenderError(
  211. f"Invalid network mode [{mode}]. Valid options are: [{', '.join(valid_modes)}] or [service:<name>]"
  212. )
  213. def valid_restart_policy_or_raise(policy: str, maximum_retry_count: int = 0):
  214. valid_restart_policies = ("always", "on-failure", "unless-stopped", "no")
  215. if policy not in valid_restart_policies:
  216. raise RenderError(
  217. f"Restart policy [{policy}] is not valid. Valid options are: [{', '.join(valid_restart_policies)}]"
  218. )
  219. if policy != "on-failure" and maximum_retry_count != 0:
  220. raise RenderError("Maximum retry count can only be set for [on-failure] restart policy")
  221. if maximum_retry_count < 0:
  222. raise RenderError("Maximum retry count must be a positive integer")
  223. return policy
  224. def valid_cap_or_raise(cap: str):
  225. valid_policies = (
  226. "ALL",
  227. "AUDIT_CONTROL",
  228. "AUDIT_READ",
  229. "AUDIT_WRITE",
  230. "BLOCK_SUSPEND",
  231. "BPF",
  232. "CHECKPOINT_RESTORE",
  233. "CHOWN",
  234. "DAC_OVERRIDE",
  235. "DAC_READ_SEARCH",
  236. "FOWNER",
  237. "FSETID",
  238. "IPC_LOCK",
  239. "IPC_OWNER",
  240. "KILL",
  241. "LEASE",
  242. "LINUX_IMMUTABLE",
  243. "MAC_ADMIN",
  244. "MAC_OVERRIDE",
  245. "MKNOD",
  246. "NET_ADMIN",
  247. "NET_BIND_SERVICE",
  248. "NET_BROADCAST",
  249. "NET_RAW",
  250. "PERFMON",
  251. "SETFCAP",
  252. "SETGID",
  253. "SETPCAP",
  254. "SETUID",
  255. "SYS_ADMIN",
  256. "SYS_BOOT",
  257. "SYS_CHROOT",
  258. "SYS_MODULE",
  259. "SYS_NICE",
  260. "SYS_PACCT",
  261. "SYS_PTRACE",
  262. "SYS_RAWIO",
  263. "SYS_RESOURCE",
  264. "SYS_TIME",
  265. "SYS_TTY_CONFIG",
  266. "SYSLOG",
  267. "WAKE_ALARM",
  268. )
  269. if cap not in valid_policies:
  270. raise RenderError(f"Capability [{cap}] is not valid. " f"Valid options are: [{', '.join(valid_policies)}]")
  271. return cap