Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cockpit: retain SELinux context if file exists #21506

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pkg/playground/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@
<p>cockpit.user() information</p>
<div id="user-info" />
</div>
<br/>
<div id="fsreplace1-div">
<h2>fsreplace1 test</h2>
<p>new filename</p>
<input id="fsreplace1-filename" />
<p>text content</p>
<input id="fsreplace1-content" />
<input id="fsreplace1-use-tag" type="checkbox" />
<label for="fsreplace1-use-tag">Use existing tag</label>
<button id="fsreplace1-create" class="pf-v5-c-button pf-m-secondary">Create file</button>
<div id="fsreplace1-error"></div>
</div>
</section>
</main>
</div>
Expand Down
21 changes: 21 additions & 0 deletions pkg/playground/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,25 @@ document.addEventListener("DOMContentLoaded", () => {

cockpit.addEventListener("visibilitychange", show_hidden);
show_hidden();

const fsreplace_btn = document.getElementById("fsreplace1-create");
const fsreplace_error = document.getElementById("fsreplace1-error");
fsreplace_btn.addEventListener("click", e => {
fsreplace_btn.disabled = true;
fsreplace_error.textContent = '';
const filename = document.getElementById("fsreplace1-filename").value;
const content = document.getElementById("fsreplace1-content").value;
const use_tag = document.getElementById("fsreplace1-use-tag").checked;
const file = cockpit.file(filename, { superuser: "try" });
martinpitt marked this conversation as resolved.
Show resolved Hide resolved

file.read().then((_content, tag) => {
file.replace(content, use_tag ? tag : undefined)
.catch(exc => {
fsreplace_error.textContent = exc.toString();
Comment on lines +147 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 3 added lines are not executed by any test.

})
.finally(() => {
fsreplace_btn.disabled = false;
});
});
});
});
24 changes: 18 additions & 6 deletions src/cockpit/channels/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,24 @@ async def set_contents(

else:
# the file must exist with the given tag
buf = os.stat(path)
if tag != tag_from_stat(buf):
raise ChannelError('change-conflict')
# chown/chmod from the existing file permissions
os.fchmod(fd, stat.S_IMODE(buf.st_mode))
os.fchown(fd, buf.st_uid, buf.st_gid)
path_fd = os.open(path, os.O_RDONLY)
martinpitt marked this conversation as resolved.
Show resolved Hide resolved

try:
buf = os.stat(path_fd)
if tag != tag_from_stat(buf):
raise ChannelError('change-conflict')
# chown/chmod from the existing file permissions
os.fchmod(fd, stat.S_IMODE(buf.st_mode))
os.fchown(fd, buf.st_uid, buf.st_gid)
try:
selinux_context = os.getxattr(path_fd, 'security.selinux')
os.setxattr(fd, 'security.selinux', selinux_context)
logger.debug("SELinux context '%s' set on '%s'", selinux_context, path)
except OSError as exc:
logger.exception("Error getting or setting SELinux context from original file: '%s'", exc)
finally:
os.close(path_fd)

os.rename(tmpname, path)
tmpname = None

Expand Down
38 changes: 38 additions & 0 deletions test/verify/check-pages
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,44 @@ OnCalendar=daily
self.assertEqual(str(user_info["id"]), m.execute("id -u admin").strip())
self.assertEqual(str(user_info["gid"]), m.execute("id -g admin").strip())

@testlib.skipImage("No SELinux", "debian-*", "ubuntu-*", "arch")
def testFSReplaceSELinuxContext(self) -> None:
b = self.browser
m = self.machine

def set_content_and_validate(path, content, custom_context):
b.set_input_text("#fsreplace1-filename", path)
b.set_input_text("#fsreplace1-content", content)
b.set_checked("#fsreplace1-use-tag", val=True)
b.click("#fsreplace1-create")
b.wait_visible("#fsreplace1-create:not(:disabled)")

self.assertEqual(m.execute(f"cat {path}"), content)
se_context = m.execute(f"stat --format=%C {path}").strip()
self.assertEqual(se_context, custom_context)

path = f"{self.vm_tmpdir}/custom-selinux-context"
custom_context = "system_u:object_r:proc_t:s0"
m.execute(f"""
touch {path}
chcon {custom_context} {path}
""")

self.login_and_go("/playground/test")
b.wait_visible("#fsreplace1-create")
set_content_and_validate(path, "data", custom_context)

# As normal user
b.drop_superuser()
self.restore_dir("/home/admin")
path = "/home/admin/custom-selinux-context-user"
custom_context = "system_u:object_r:proc_t:s0"
m.execute("runuser -u admin -- sh -ex", input=f"touch {path}")
m.execute(f"chcon {custom_context} {path}")

b.wait_visible("#fsreplace1-create")
set_content_and_validate(path, "user data", custom_context)


if __name__ == '__main__':
testlib.test_main()
Loading