Files
podman-compose/tests/unit/test_container_to_args_secrets.py
Jaroslav Henner 82d7622c45 Add relabel option to secrets
On selinux enabled system, the secrets cannot be read without proper
relabeling or correct policy being set.

This patch enables user to instruc podman-copose to use :z or :Z podman
volume options to make podman relabel the file under bind-mount.

More info here:
https://unix.stackexchange.com/questions/728801/host-wide-consequences-of-setting-selinux-z-z-option-on-container-bind-mounts?rq=1

Signed-off-by: Jaroslav Henner <1187265+jarovo@users.noreply.github.com>
2025-06-05 00:13:58 +02:00

413 lines
13 KiB
Python

# SPDX-License-Identifier: GPL-2.0
import os
import unittest
from parameterized import parameterized
from podman_compose import container_to_args
from tests.unit.test_container_to_args import create_compose_mock
from tests.unit.test_container_to_args import get_minimal_container
def repo_root():
return os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
class TestContainerToArgsSecrets(unittest.IsolatedAsyncioTestCase):
async def test_pass_secret_as_env_variable(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret": {"external": "true"} # must have external or name value
}
cnt = get_minimal_container()
cnt["secrets"] = [
{
"source": "my_secret",
"target": "ENV_SECRET",
"type": "env",
},
]
args = await container_to_args(c, cnt)
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret,type=env,target=ENV_SECRET",
"busybox",
],
)
async def test_secret_as_env_external_true_has_no_name(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret": {
"name": "my_secret", # must have external or name value
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{
"source": "my_secret",
"target": "ENV_SECRET",
"type": "env",
}
]
args = await container_to_args(c, cnt)
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret,type=env,target=ENV_SECRET",
"busybox",
],
)
async def test_pass_secret_as_env_variable_no_external(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret": {} # must have external or name value
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{
"source": "my_secret",
"target": "ENV_SECRET",
"type": "env",
}
]
with self.assertRaises(ValueError) as context:
await container_to_args(c, cnt)
self.assertIn('ERROR: unparsable secret: ', str(context.exception))
@parameterized.expand([
(
"secret_no_name",
{"my_secret": "my_secret_name", "external": "true"},
{}, # must have a name
),
(
"no_secret_name_in_declared_secrets",
{}, # must have a name
{
"source": "my_secret_name",
},
),
(
"secret_name_does_not_match_declared_secrets_name",
{
"wrong_name": "my_secret_name",
},
{
"source": "name", # secret name must match the one in declared_secrets
},
),
(
"secret_name_empty_string",
{"": "my_secret_name"},
{
"source": "", # can not be empty string
},
),
])
async def test_secret_name(self, test_name, declared_secrets, add_to_minimal_container):
c = create_compose_mock()
c.declared_secrets = declared_secrets
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [add_to_minimal_container]
with self.assertRaises(ValueError) as context:
await container_to_args(c, cnt)
self.assertIn('ERROR: undeclared secret: ', str(context.exception))
async def test_secret_string_no_external_name_in_declared_secrets(self):
c = create_compose_mock()
c.declared_secrets = {"my_secret_name": {"external": "true"}}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
"my_secret_name",
]
args = await container_to_args(c, cnt)
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret_name",
"busybox",
],
)
async def test_secret_string_options_external_name_in_declared_secrets(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret_name": {
"external": "true",
"name": "my_secret_name",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{
"source": "my_secret_name",
"target": "my_secret_name",
"uid": "103",
"gid": "103",
"mode": "400",
}
]
with self.assertLogs() as cm:
args = await container_to_args(c, cnt)
self.assertEqual(len(cm.output), 1)
self.assertIn('That is un-supported and a no-op and is ignored.', cm.output[0])
self.assertIn('my_secret_name', cm.output[0])
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret_name,uid=103,gid=103,mode=400",
"busybox",
],
)
async def test_secret_string_external_name_in_declared_secrets_does_not_match_secret(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret_name": {
"external": "true",
"name": "wrong_secret_name",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
"my_secret_name",
]
with self.assertRaises(ValueError) as context:
await container_to_args(c, cnt)
self.assertIn('ERROR: Custom name/target reference ', str(context.exception))
async def test_secret_target_does_not_match_secret_name_secret_type_not_env(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret_name": {
"external": "true",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{
"source": "my_secret_name",
"target": "does_not_equal_secret_name",
"type": "does_not_equal_env",
}
]
with self.assertRaises(ValueError) as context:
await container_to_args(c, cnt)
self.assertIn('ERROR: Custom name/target reference ', str(context.exception))
async def test_secret_target_does_not_match_secret_name_secret_type_env(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret_name": {
"external": "true",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{"source": "my_secret_name", "target": "does_not_equal_secret_name", "type": "env"}
]
args = await container_to_args(c, cnt)
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret_name,type=env,target=does_not_equal_secret_name",
"busybox",
],
)
async def test_secret_target_matches_secret_name_secret_type_not_env(self):
c = create_compose_mock()
c.declared_secrets = {
"my_secret_name": {
"external": "true",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{"source": "my_secret_name", "target": "my_secret_name", "type": "does_not_equal_env"}
]
with self.assertLogs() as cm:
args = await container_to_args(c, cnt)
self.assertEqual(len(cm.output), 1)
self.assertIn('That is un-supported and a no-op and is ignored.', cm.output[0])
self.assertIn('my_secret_name', cm.output[0])
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--secret",
"my_secret_name,type=does_not_equal_env",
"busybox",
],
)
@parameterized.expand([
(
"no_secret_target",
{
"file_secret": {
"file": "./my_secret",
}
},
"file_secret",
repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind",
),
(
"relabel",
{"file_secret": {"file": "./my_secret", "x-podman.relabel": "Z"}},
"file_secret",
repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind,Z",
),
(
"relabel",
{"file_secret": {"file": "./my_secret", "x-podman.relabel": "z"}},
"file_secret",
repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind,z",
),
(
"custom_target_name",
{
"file_secret": {
"file": "./my_secret",
}
},
{
"source": "file_secret",
"target": "custom_name",
},
repo_root() + "/test_dirname/my_secret:/run/secrets/custom_name:ro,rprivate,rbind",
),
(
"no_custom_target_name",
{
"file_secret": {
"file": "./my_secret",
}
},
{
"source": "file_secret",
},
repo_root() + "/test_dirname/my_secret:/run/secrets/file_secret:ro,rprivate,rbind",
),
(
"custom_location",
{
"file_secret": {
"file": "./my_secret",
}
},
{
"source": "file_secret",
"target": "/etc/custom_location",
},
repo_root() + "/test_dirname/my_secret:/etc/custom_location:ro,rprivate,rbind",
),
])
async def test_file_secret(
self, test_name, declared_secrets, add_to_minimal_container, expected_volume_ref
):
c = create_compose_mock()
c.declared_secrets = declared_secrets
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [add_to_minimal_container]
args = await container_to_args(c, cnt)
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--volume",
expected_volume_ref,
"busybox",
],
)
async def test_file_secret_unused_params_warning(self):
c = create_compose_mock()
c.declared_secrets = {
"file_secret": {
"file": "./my_secret",
}
}
cnt = get_minimal_container()
cnt["_service"] = "test-service"
cnt["secrets"] = [
{
"source": "file_secret",
"target": "unused_params_warning",
"uid": "103",
"gid": "103",
"mode": "400",
}
]
with self.assertLogs() as cm:
args = await container_to_args(c, cnt)
self.assertEqual(len(cm.output), 1)
self.assertIn('with uid, gid, or mode.', cm.output[0])
self.assertIn('unused_params_warning', cm.output[0])
self.assertEqual(
args,
[
"--name=project_name_service_name1",
"-d",
"--network=bridge:alias=service_name",
"--volume",
repo_root()
+ "/test_dirname/my_secret:/run/secrets/unused_params_warning:ro,rprivate,rbind",
"busybox",
],
)