mirror of
https://github.com/MAGICGrants/truenas-apps.git
synced 2026-01-09 20:47:58 -05:00
library: change sec-opt func and adapt apps (#1569)
* change sec-opt func and adapt apps * add test
This commit is contained in:
@@ -27,8 +27,8 @@ icon: https://media.sys.truenas.net/apps/glances/icons/icon.png
|
||||
keywords:
|
||||
- metric
|
||||
- monitoring
|
||||
lib_version: 2.1.14
|
||||
lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
|
||||
lib_version: 2.1.15
|
||||
lib_version_hash: e26cffb91766782c787867b379519f7407b1fed8450dce463cefa56340a41d84
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
@@ -46,4 +46,4 @@ sources:
|
||||
- https://hub.docker.com/r/nicolargo/glances
|
||||
title: Glances
|
||||
train: community
|
||||
version: 1.0.3
|
||||
version: 1.0.4
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
{% do glances_container.add_caps(["FOWNER", "DAC_OVERRIDE", "SETGID", "SETUID", "SYS_PTRACE"]) %}
|
||||
{% do glances_container.remove_security_opt("no-new-privileges") %}
|
||||
{% do glances_container.add_security_opt("apparmor=unconfined") %}
|
||||
{% do glances_container.add_security_opt("apparmor", "unconfined") %}
|
||||
|
||||
{% do glances_container.healthcheck.set_test("curl", {"port": values.network.web_port.port_number, "path": "/api/4/status"}) %}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ try:
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from .security_opts import SecurityOpts
|
||||
from .storage import Storage
|
||||
from .sysctls import Sysctls
|
||||
except ImportError:
|
||||
@@ -52,6 +53,7 @@ except ImportError:
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from security_opts import SecurityOpts
|
||||
from storage import Storage
|
||||
from sysctls import Sysctls
|
||||
|
||||
@@ -73,7 +75,7 @@ class Container:
|
||||
self._hostname: str = ""
|
||||
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
|
||||
self._cap_add: set[str] = set()
|
||||
self._security_opt: set[str] = set(["no-new-privileges"])
|
||||
self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
|
||||
self._privileged: bool = False
|
||||
self._group_add: set[int | str] = set()
|
||||
self._network_mode: str = ""
|
||||
@@ -227,13 +229,11 @@ class Container:
|
||||
raise RenderError(f"Capability [{c}] already added")
|
||||
self._cap_add.add(valid_cap_or_raise(c))
|
||||
|
||||
def add_security_opt(self, opt: str):
|
||||
if opt in self._security_opt:
|
||||
raise RenderError(f"Security Option [{opt}] already added")
|
||||
self._security_opt.add(opt)
|
||||
def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._security_opt.add_opt(key, value, arg)
|
||||
|
||||
def remove_security_opt(self, opt: str):
|
||||
self._security_opt.remove(opt)
|
||||
def remove_security_opt(self, key: str):
|
||||
self._security_opt.remove_opt(key)
|
||||
|
||||
def set_network_mode(self, mode: str):
|
||||
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
|
||||
@@ -366,8 +366,8 @@ class Container:
|
||||
if self._cap_add:
|
||||
result["cap_add"] = sorted(self._cap_add)
|
||||
|
||||
if self._security_opt:
|
||||
result["security_opt"] = sorted(self._security_opt)
|
||||
if self._security_opt.has_opts():
|
||||
result["security_opt"] = self._security_opt.render()
|
||||
|
||||
if self._network_mode:
|
||||
result["network_mode"] = self._network_mode
|
||||
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .validations import valid_security_opt_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from validations import valid_security_opt_or_raise
|
||||
|
||||
|
||||
class SecurityOpt:
|
||||
def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._opt: str = valid_security_opt_or_raise(opt)
|
||||
self._value = str(value).lower() if isinstance(value, bool) else value
|
||||
self._arg: str | None = arg
|
||||
|
||||
def render(self):
|
||||
result = self._opt
|
||||
if self._value is not None:
|
||||
result = f"{result}={self._value}"
|
||||
if self._arg is not None:
|
||||
result = f"{result}:{self._arg}"
|
||||
return result
|
||||
|
||||
|
||||
class SecurityOpts:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._opts: dict[str, SecurityOpt] = dict()
|
||||
self.add_opt("no-new-privileges", True)
|
||||
|
||||
def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
|
||||
if key in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] already added")
|
||||
self._opts[key] = SecurityOpt(key, value, arg)
|
||||
|
||||
def remove_opt(self, key: str):
|
||||
if key not in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] not found")
|
||||
del self._opts[key]
|
||||
|
||||
def has_opts(self):
|
||||
return len(self._opts) > 0
|
||||
|
||||
def render(self):
|
||||
result = []
|
||||
for opt in sorted(self._opts.values(), key=lambda o: o._opt):
|
||||
result.append(opt.render())
|
||||
return result
|
||||
@@ -250,35 +250,6 @@ def test_invalid_caps(mock_values):
|
||||
c1.add_caps(["invalid_cap"])
|
||||
|
||||
|
||||
def test_remove_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert "security_opt" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_add_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("seccomp=unconfined")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == [
|
||||
"no-new-privileges",
|
||||
"seccomp=unconfined",
|
||||
]
|
||||
|
||||
|
||||
def test_add_duplicate_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges")
|
||||
|
||||
|
||||
def test_network_mode(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
@@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
|
||||
|
||||
from render import Render
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_values():
|
||||
return {
|
||||
"images": {
|
||||
"test_image": {
|
||||
"repository": "nginx",
|
||||
"tag": "latest",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_add_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("apparmor", "unconfined")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == ["apparmor=unconfined", "no-new-privileges=true"]
|
||||
|
||||
|
||||
def test_add_duplicate_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges", True)
|
||||
|
||||
|
||||
def test_add_empty_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("", True)
|
||||
|
||||
|
||||
def test_remove_security_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
output = render.render()
|
||||
assert "security_opt" not in output["services"]["test_container"]
|
||||
|
||||
|
||||
def test_add_security_opt_boolean(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
c1.add_security_opt("no-new-privileges", False)
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == ["no-new-privileges=false"]
|
||||
|
||||
|
||||
def test_add_security_opt_arg(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.add_security_opt("label", "type", "svirt_apache_t")
|
||||
output = render.render()
|
||||
assert output["services"]["test_container"]["security_opt"] == [
|
||||
"label=type:svirt_apache_t",
|
||||
"no-new-privileges=true",
|
||||
]
|
||||
|
||||
|
||||
def test_add_security_opt_with_invalid_opt(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("invalid")
|
||||
|
||||
|
||||
def test_add_security_opt_with_opt_containing_value(mock_values):
|
||||
render = Render(mock_values)
|
||||
c1 = render.add_container("test_container", "test_image")
|
||||
c1.healthcheck.disable()
|
||||
c1.remove_security_opt("no-new-privileges")
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("no-new-privileges=true")
|
||||
with pytest.raises(Exception):
|
||||
c1.add_security_opt("apparmor:unconfined")
|
||||
@@ -24,6 +24,16 @@ RESTRICTED: tuple[Path, ...] = (
|
||||
)
|
||||
|
||||
|
||||
def valid_security_opt_or_raise(opt: str):
|
||||
if ":" in opt or "=" in opt:
|
||||
raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")
|
||||
valid_opts = ["apparmor", "no-new-privileges", "seccomp", "systempaths", "label"]
|
||||
if opt not in valid_opts:
|
||||
raise RenderError(f"Security Option [{opt}] is not valid. Valid options are: [{', '.join(valid_opts)}]")
|
||||
|
||||
return opt
|
||||
|
||||
|
||||
def valid_port_bind_mode_or_raise(status: str):
|
||||
valid_statuses = ("published", "exposed", "")
|
||||
if status not in valid_statuses:
|
||||
@@ -42,8 +42,8 @@ icon: https://media.sys.truenas.net/apps/steam-headless/icons/icon.png
|
||||
keywords:
|
||||
- games
|
||||
- steam
|
||||
lib_version: 2.1.14
|
||||
lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
|
||||
lib_version: 2.1.15
|
||||
lib_version_hash: e26cffb91766782c787867b379519f7407b1fed8450dce463cefa56340a41d84
|
||||
maintainers:
|
||||
- email: dev@ixsystems.com
|
||||
name: truenas
|
||||
@@ -60,4 +60,4 @@ sources:
|
||||
- https://github.com/Steam-Headless/docker-steam-headless
|
||||
title: Steam Headless
|
||||
train: community
|
||||
version: 1.0.6
|
||||
version: 1.0.7
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
{% do c1.set_ipc_mode("host") %}
|
||||
{% do c1.add_device_cgroup_rule("c 13:* rwm") %}
|
||||
{% do c1.remove_security_opt("no-new-privileges") %}
|
||||
{% do c1.add_security_opt("seccomp:unconfined") %}
|
||||
{% do c1.add_security_opt("apparmor:unconfined") %}
|
||||
{% do c1.add_security_opt("seccomp", "unconfined") %}
|
||||
{% do c1.add_security_opt("apparmor", "unconfined") %}
|
||||
{% do c1.add_caps([
|
||||
"AUDIT_WRITE",
|
||||
"CHOWN",
|
||||
|
||||
@@ -27,6 +27,7 @@ try:
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from .security_opts import SecurityOpts
|
||||
from .storage import Storage
|
||||
from .sysctls import Sysctls
|
||||
except ImportError:
|
||||
@@ -52,6 +53,7 @@ except ImportError:
|
||||
valid_port_bind_mode_or_raise,
|
||||
valid_pull_policy_or_raise,
|
||||
)
|
||||
from security_opts import SecurityOpts
|
||||
from storage import Storage
|
||||
from sysctls import Sysctls
|
||||
|
||||
@@ -73,7 +75,7 @@ class Container:
|
||||
self._hostname: str = ""
|
||||
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
|
||||
self._cap_add: set[str] = set()
|
||||
self._security_opt: set[str] = set(["no-new-privileges"])
|
||||
self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
|
||||
self._privileged: bool = False
|
||||
self._group_add: set[int | str] = set()
|
||||
self._network_mode: str = ""
|
||||
@@ -227,13 +229,11 @@ class Container:
|
||||
raise RenderError(f"Capability [{c}] already added")
|
||||
self._cap_add.add(valid_cap_or_raise(c))
|
||||
|
||||
def add_security_opt(self, opt: str):
|
||||
if opt in self._security_opt:
|
||||
raise RenderError(f"Security Option [{opt}] already added")
|
||||
self._security_opt.add(opt)
|
||||
def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._security_opt.add_opt(key, value, arg)
|
||||
|
||||
def remove_security_opt(self, opt: str):
|
||||
self._security_opt.remove(opt)
|
||||
def remove_security_opt(self, key: str):
|
||||
self._security_opt.remove_opt(key)
|
||||
|
||||
def set_network_mode(self, mode: str):
|
||||
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
|
||||
@@ -366,8 +366,8 @@ class Container:
|
||||
if self._cap_add:
|
||||
result["cap_add"] = sorted(self._cap_add)
|
||||
|
||||
if self._security_opt:
|
||||
result["security_opt"] = sorted(self._security_opt)
|
||||
if self._security_opt.has_opts():
|
||||
result["security_opt"] = self._security_opt.render()
|
||||
|
||||
if self._network_mode:
|
||||
result["network_mode"] = self._network_mode
|
||||
@@ -0,0 +1,52 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from render import Render
|
||||
|
||||
try:
|
||||
from .error import RenderError
|
||||
from .validations import valid_security_opt_or_raise
|
||||
except ImportError:
|
||||
from error import RenderError
|
||||
from validations import valid_security_opt_or_raise
|
||||
|
||||
|
||||
class SecurityOpt:
|
||||
def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
|
||||
self._opt: str = valid_security_opt_or_raise(opt)
|
||||
self._value = str(value).lower() if isinstance(value, bool) else value
|
||||
self._arg: str | None = arg
|
||||
|
||||
def render(self):
|
||||
result = self._opt
|
||||
if self._value is not None:
|
||||
result = f"{result}={self._value}"
|
||||
if self._arg is not None:
|
||||
result = f"{result}:{self._arg}"
|
||||
return result
|
||||
|
||||
|
||||
class SecurityOpts:
|
||||
def __init__(self, render_instance: "Render"):
|
||||
self._render_instance = render_instance
|
||||
self._opts: dict[str, SecurityOpt] = dict()
|
||||
self.add_opt("no-new-privileges", True)
|
||||
|
||||
def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
|
||||
if key in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] already added")
|
||||
self._opts[key] = SecurityOpt(key, value, arg)
|
||||
|
||||
def remove_opt(self, key: str):
|
||||
if key not in self._opts:
|
||||
raise RenderError(f"Security Option [{key}] not found")
|
||||
del self._opts[key]
|
||||
|
||||
def has_opts(self):
|
||||
return len(self._opts) > 0
|
||||
|
||||
def render(self):
|
||||
result = []
|
||||
for opt in sorted(self._opts.values(), key=lambda o: o._opt):
|
||||
result.append(opt.render())
|
||||
return result
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user