1# Original code:
2# Copyright (c) Jupyter Development Team.
3# Distributed under the terms of the Modified BSD License.
4import time
5
6import pytest
7
8PROG_LIST_TO_CHECK = (
9    "afni",
10    "RetroTS.py",
11    "align_epi_anat.py",
12    "3dinfo",
13    )
14
15def test_uid_change(container):
16    """Container should change the UID of the default user."""
17    c = container.run(
18        tty=True,
19        user='root',
20        environment=['CONTAINER_UID=1010'],
21        command=['start.sh', 'bash', '-c', 'id && touch /home/afni_user/test-file']
22    )
23    # usermod is slow so give it some time
24    c.wait(timeout=120)
25    stdout = c.logs(stdout=True).decode('utf-8')
26    if 'uid=1010(afni_user)' not in stdout:
27        print(stdout)
28        raise EnvironmentError(
29            "User id is not being changed correctly"
30        )
31
32
33
34def test_gid_change(container):
35    """Container should change the GID of the default user."""
36    c = container.run(
37        tty=True,
38        user='root',
39        environment=['CONTAINER_GID=110'],
40        command=['start.sh', 'id']
41    )
42    c.wait(timeout=10)
43    logs = c.logs(stdout=True).decode('utf-8')
44    assert 'gid=110(afni_user)' in logs
45    assert 'groups=110(afni_user),100(users)' in logs
46
47
48def test_chown_extra(container):
49    """Container should change the UID/GID of CHOWN_EXTRA."""
50    c = container.run(
51        tty=True,
52        user='root',
53        environment=['CONTAINER_UID=1010',
54                     'CONTAINER_GID=101',
55                     'CHOWN_EXTRA=/usr/bin',
56                     'CHOWN_EXTRA_OPTS=-R',
57        ],
58        command=['start.sh', 'bash', '-c', 'stat -c \'%n:%u:%g\' /usr/bin/python']
59    )
60    # chown is slow so give it some time
61    c.wait(timeout=5)
62    if not '/usr/bin/python:1010:101' in c.logs(stdout=True).decode('utf-8'):
63        print(c.logs(stdout=True).decode('utf-8'))
64        raise EnvironmentError(
65            "Expected to find an executable python in the container"
66        )
67
68
69def test_chown_home(container):
70    """Container should change the CONTAINER_USER home directory owner and
71    group to the current value of CONTAINER_UID and CONTAINER_GID."""
72    c = container.run(
73        tty=True,
74        user='root',
75        environment=['CONTAINER_UID=1010',
76                     'CONTAINER_GID=101',
77                     'CHOWN_HOME=yes',
78                     'CHOWN_HOME_OPTS=-R',
79        ],
80        command=['start.sh', 'bash', '-c', 'stat -c \'%n:%u:%g\' /home/afni_user']
81    )
82    c.wait(timeout=2)
83    assert "Changing ownership of /home/afni_user to 1010:101 with options '-R'" in c.logs(stdout=True).decode('utf-8')
84
85
86def test_sudo(container):
87    """Container should grant passwordless sudo to the default user."""
88    c = container.run(
89        tty=True,
90        user='root',
91        environment=['GRANT_SUDO=yes'],
92        command=['start.sh', 'sudo', 'id']
93    )
94    rv = c.wait(timeout=10)
95    assert rv == 0 or rv["StatusCode"] == 0
96    assert 'uid=0(root)' in c.logs(stdout=True).decode('utf-8')
97
98
99def test_sudo_path(container):
100    """Container should have usable /usr/local/bin in the sudo secure_path."""
101    c = container.run(
102        tty=True,
103        user='root',
104        environment=['GRANT_SUDO=yes'],
105        command=['start.sh', 'sudo', 'which', 'apt-get']
106    )
107    rv = c.wait(timeout=10)
108    assert rv == 0 or rv["StatusCode"] == 0
109    assert c.logs(stdout=True).decode('utf-8').rstrip().endswith('/usr/local/bin/apt-get')
110
111
112def test_sudo_path_without_grant(container):
113    """Container should have usable /usr/local/bin in the unprivileged user"""
114    c = container.run(
115        tty=True,
116        user='root',
117        command=['start.sh', 'which', 'apt-get']
118    )
119    rv = c.wait(timeout=10)
120    assert rv == 0 or rv["StatusCode"] == 0
121    assert c.logs(stdout=True).decode('utf-8').rstrip().endswith('/usr/local/bin/apt-get')
122
123
124def test_group_add(container, tmpdir):
125    """Container should run with the specified uid, gid, and secondary
126    group.
127    """
128    c = container.run(
129        user='1010:1010',
130        group_add=['users'],
131        command=['start.sh', 'id']
132    )
133    rv = c.wait(timeout=5)
134    assert rv == 0 or rv["StatusCode"] == 0
135    expected = 'uid=1010 gid=1010 groups=1010,100(users)'
136    assert expected in c.logs(stdout=True).decode('utf-8')
137
138@pytest.mark.parametrize(
139    "env_tweaks",
140    (
141        ['GRANT_SUDO=yes'],
142        ['CONTAINER_UID=1010'],
143        ['CONTAINER_UID=1010','CONTAINER_GID=120','CHOWN_EXTRA=/opt','CHOWN_EXTRA_OPTS=-R'],
144    )
145)
146@pytest.mark.parametrize(
147    "named_container",
148    (
149        "afni/afni_make_build",
150        "afni/afni_cmake_build",
151    ),
152    indirect=True,
153)
154def test_various_programs_are_found(named_container,env_tweaks):
155    """Working containers should be able to find the installed binaries, scripts,etc"""
156
157    c = named_container.run(
158        tty=True,
159        user='root',
160        environment=[*env_tweaks],
161        detach=True
162    )
163    # rv = c.wait(timeout=120)
164    # assert rv == 0 or rv["StatusCode"] == 0
165    # Check that each program is available (on the PATH and executable)
166    for prog in PROG_LIST_TO_CHECK:
167        res = c.exec_run(["which", prog])
168        if not res.output.decode('utf-8').rstrip().endswith(prog):
169            print(res.output.decode('utf-8').rstrip())
170            raise EnvironmentError(f"Could not find the '{prog}' executable in {named_container.image_name}")
171
172
173
174@pytest.mark.parametrize( "named_container", ( "afni/afni_make_build",
175                                              "afni/afni_cmake_build",),
176                         indirect=True,)
177def test_singularity_like_restrictions(named_container):
178    """Working containers should be able to find the installed binaries,
179    scripts,etc even with restricted access"""
180    c = named_container.run(
181        tty=True,
182        detach=True,
183        read_only=True,
184        tmpfs = {'/run':'', '/tmp':''},
185    )
186    # Check that each program is available (on the PATH and executable)
187    for prog in PROG_LIST_TO_CHECK:
188        res = c.exec_run(["which", prog])
189        assert res.output.decode('utf-8').rstrip().endswith(prog)
190
191
192