diff --git a/include/multipass/utils/permission_utils.h b/include/multipass/utils/permission_utils.h index 3f72be9867e..6bb35a56bd9 100644 --- a/include/multipass/utils/permission_utils.h +++ b/include/multipass/utils/permission_utils.h @@ -18,10 +18,11 @@ #ifndef MULTIPASS_PERMISSION_UTILS_H #define MULTIPASS_PERMISSION_UTILS_H -#include -#include #include +#include +#include + #define MP_PERMISSIONS multipass::PermissionUtils::instance() namespace multipass @@ -30,13 +31,15 @@ namespace multipass class PermissionUtils : public Singleton { public: + using Path = std::filesystem::path; + PermissionUtils(const PrivatePass&) noexcept; - virtual void set_permissions(const fs::path& path, const QFileDevice::Permissions& permissions) const; - virtual void take_ownership(const fs::path& path, bool root = true) const; + virtual void set_permissions(const Path& path, const QFileDevice::Permissions& permissions) const; + virtual void take_ownership(const Path& path, bool root = true) const; // sets owner to root and sets permissions such that only owner has access. - virtual void restrict_permissions(const fs::path& path) const; + virtual void restrict_permissions(const Path& path) const; }; } // namespace multipass diff --git a/src/utils/permission_utils.cpp b/src/utils/permission_utils.cpp index 14a37d6cb4f..31004e9ff24 100644 --- a/src/utils/permission_utils.cpp +++ b/src/utils/permission_utils.cpp @@ -17,12 +17,17 @@ #include +#include +#include + namespace mp = multipass; namespace fs = mp::fs; +using Path = mp::PermissionUtils::Path; + namespace { -void set_single_permissions(const fs::path& path, const QFileDevice::Permissions& permissions) +void set_single_permissions(const Path& path, const QFileDevice::Permissions& permissions) { QString qpath = QString::fromUtf8(path.u8string()); @@ -30,7 +35,7 @@ void set_single_permissions(const fs::path& path, const QFileDevice::Permissions throw std::runtime_error(fmt::format("Cannot set permissions for '{}'", path.string())); } -void set_single_owner(const fs::path& path, bool root) +void set_single_owner(const Path& path, bool root) { QString qpath = QString::fromUtf8(path.u8string()); @@ -39,7 +44,7 @@ void set_single_owner(const fs::path& path, bool root) } // only exists because MP_FILEOPS doesn't overload the throwing variaions of std::filesystem functions -void throw_if_error(const fs::path& path, const std::error_code& ec) +void throw_if_error(const Path& path, const std::error_code& ec) { if (ec) throw std::system_error( @@ -49,7 +54,7 @@ void throw_if_error(const fs::path& path, const std::error_code& ec) // recursively iterates over all files in folder and applies a function that takes a path template -void apply_on_files(const fs::path& path, Func&& func) +void apply_on_files(const Path& path, Func&& func) { std::error_code ec{}; if (!MP_FILEOPS.exists(path, ec) || ec) @@ -63,7 +68,7 @@ void apply_on_files(const fs::path& path, Func&& func) auto dir_iterator = MP_FILEOPS.recursive_dir_iterator(path, ec); throw_if_error(path, ec); - if (!dir_iterator) + if (!dir_iterator) [[unlikely]] throw std::runtime_error(fmt::format("Cannot iterate over directory '{}'", path.string())); while (dir_iterator->hasNext()) @@ -83,17 +88,17 @@ mp::PermissionUtils::PermissionUtils(const Singleton::PrivatePa { } -void mp::PermissionUtils::set_permissions(const fs::path& path, const QFileDevice::Permissions& permissions) const +void mp::PermissionUtils::set_permissions(const Path& path, const QFileDevice::Permissions& permissions) const { apply_on_files(path, [&](const fs::path& apply_path) { set_single_permissions(apply_path, permissions); }); } -void mp::PermissionUtils::take_ownership(const fs::path& path, bool root) const +void mp::PermissionUtils::take_ownership(const Path& path, bool root) const { apply_on_files(path, [&](const fs::path& apply_path) { set_single_owner(apply_path, root); }); } -void mp::PermissionUtils::restrict_permissions(const fs::path& path) const +void mp::PermissionUtils::restrict_permissions(const Path& path) const { apply_on_files(path, [&](const fs::path& apply_path) { set_single_owner(apply_path, true); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b201a931ae6..a1e24a761d9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -124,6 +124,7 @@ add_executable(multipass_tests test_sftp_utils.cpp test_file_ops.cpp test_recursive_dir_iter.cpp + test_permission_utils.cpp ) target_include_directories(multipass_tests diff --git a/tests/mock_permission_utils.h b/tests/mock_permission_utils.h index 69a8b07aba3..0d630a19d58 100644 --- a/tests/mock_permission_utils.h +++ b/tests/mock_permission_utils.h @@ -35,10 +35,10 @@ class MockPermissionUtils : public PermissionUtils MOCK_METHOD(void, set_permissions, - (const fs::path& path, const QFileDevice::Permissions& permissions), + (const Path& path, const QFileDevice::Permissions& permissions), (const, override)); - MOCK_METHOD(void, take_ownership, (const fs::path& path, bool root), (const, override)); - MOCK_METHOD(void, restrict_permissions, (const fs::path& path), (const, override)); + MOCK_METHOD(void, take_ownership, (const Path& path, bool root), (const, override)); + MOCK_METHOD(void, restrict_permissions, (const Path& path), (const, override)); MP_MOCK_SINGLETON_BOILERPLATE(MockPermissionUtils, PermissionUtils); }; diff --git a/tests/test_permission_utils.cpp b/tests/test_permission_utils.cpp new file mode 100644 index 00000000000..ae132b93eef --- /dev/null +++ b/tests/test_permission_utils.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include + +#include "mock_file_ops.h" +#include "mock_permission_utils.h" +#include "mock_platform.h" +#include "mock_recursive_dir_iterator.h" + +namespace mp = multipass; +namespace mpt = mp::test; +using namespace testing; + +namespace +{ +struct TestPermissionUtils : public Test +{ + const mpt::MockFileOps::GuardedMock guarded_mock_file_ops = mpt::MockFileOps::inject(); + mpt::MockFileOps& mock_file_ops = *guarded_mock_file_ops.first; + const mpt::MockPlatform::GuardedMock guarded_mock_platform = mpt::MockPlatform::inject(); + mpt::MockPlatform& mock_platform = *guarded_mock_platform.first; + + const mp::PermissionUtils::Path test_path{"test_path"}; + const QString test_qpath{QString::fromUtf8(test_path.u8string())}; + static constexpr QFileDevice::Permissions test_permissions{QFileDevice::Permission::ReadOwner}; + static constexpr QFileDevice::Permissions restricted_permissions{QFileDevice::Permission::ReadOwner | + QFileDevice::Permission::WriteOwner}; +}; + +struct TestPermissionUtilsNoFile : public TestPermissionUtils +{ + TestPermissionUtilsNoFile() + { + EXPECT_CALL(mock_file_ops, exists(test_path, _)).WillOnce(Return(false)); + } +}; + +TEST_F(TestPermissionUtilsNoFile, set_permissions_throws_when_file_non_existant) +{ + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.set_permissions(test_path, test_permissions), + std::runtime_error, + mpt::match_what(HasSubstr(test_path.string()))); +} + +TEST_F(TestPermissionUtilsNoFile, take_ownership_throws_when_file_non_existant) +{ + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.take_ownership(test_path), + std::runtime_error, + mpt::match_what(HasSubstr(test_path.string()))); +} + +TEST_F(TestPermissionUtilsNoFile, restrict_permissions_throws_when_file_non_existant) +{ + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.restrict_permissions(test_path), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("nonexistent file"), HasSubstr(test_path.string())))); +} + +struct TestPermissionUtilsFile : public TestPermissionUtils +{ + TestPermissionUtilsFile() + { + EXPECT_CALL(mock_file_ops, exists(test_path, _)).WillOnce(Return(true)); + EXPECT_CALL(mock_file_ops, is_directory(test_path, _)).WillRepeatedly(Return(false)); + } +}; + +TEST_F(TestPermissionUtilsFile, set_permissions_sets_permissions) +{ + EXPECT_CALL(mock_platform, set_permissions(test_qpath, test_permissions)).WillOnce(Return(true)); + + MP_PERMISSIONS.set_permissions(test_path, test_permissions); +} + +TEST_F(TestPermissionUtilsFile, set_permissions_throws_on_failure) +{ + EXPECT_CALL(mock_platform, set_permissions(test_qpath, test_permissions)).WillOnce(Return(false)); + + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.set_permissions(test_path, test_permissions), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Cannot set permissions"), HasSubstr(test_path.string())))); +} + +TEST_F(TestPermissionUtilsFile, take_ownership_takes_ownership) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, true)).WillOnce(Return(true)); + + MP_PERMISSIONS.take_ownership(test_path, true); +} + +TEST_F(TestPermissionUtilsFile, take_ownership_takes_user_ownership) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, false)).WillOnce(Return(true)); + + MP_PERMISSIONS.take_ownership(test_path, false); +} + +TEST_F(TestPermissionUtilsFile, take_ownership_throws_on_failure) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, _)).WillOnce(Return(false)); + + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.take_ownership(test_path), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Cannot set owner"), HasSubstr(test_path.string())))); +} + +TEST_F(TestPermissionUtilsFile, restrict_permissions_restricts_permissions) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, true)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, set_permissions(test_qpath, restricted_permissions)).WillOnce(Return(true)); + + MP_PERMISSIONS.restrict_permissions(test_path); +} + +struct TestPermissionUtilsDir : public TestPermissionUtils +{ + TestPermissionUtilsDir() + { + EXPECT_CALL(mock_file_ops, exists(test_path, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(mock_file_ops, is_directory(test_path, _)).WillOnce(Return(true)); + + EXPECT_CALL(entry1, path).WillRepeatedly(ReturnRef(path1)); + EXPECT_CALL(entry2, path).WillRepeatedly(ReturnRef(path2)); + + auto iter = std::make_unique(); + auto iter_p = iter.get(); + + EXPECT_CALL(*iter_p, hasNext).WillOnce(Return(true)).WillOnce(Return(true)).WillRepeatedly(Return(false)); + EXPECT_CALL(*iter_p, next).WillOnce(ReturnRef(entry1)).WillOnce(ReturnRef(entry2)); + EXPECT_CALL(mock_file_ops, recursive_dir_iterator(test_path, _)).WillOnce(Return(std::move(iter))); + } + + const mp::PermissionUtils::Path path1{"Hello.txt"}; + const QString qpath1{QString::fromUtf8(path1.u8string())}; + mpt::MockDirectoryEntry entry1; + + const mp::PermissionUtils::Path path2{"World.txt"}; + const QString qpath2{QString::fromUtf8(path2.u8string())}; + mpt::MockDirectoryEntry entry2; +}; + +TEST_F(TestPermissionUtilsDir, set_permissions_iterates_dir) +{ + EXPECT_CALL(mock_platform, set_permissions(test_qpath, test_permissions)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, set_permissions(qpath1, test_permissions)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, set_permissions(qpath2, test_permissions)).WillOnce(Return(true)); + + MP_PERMISSIONS.set_permissions(test_path, test_permissions); +} + +TEST_F(TestPermissionUtilsDir, take_ownership_iterates_dir) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, false)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, take_ownership(qpath1, false)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, take_ownership(qpath2, false)).WillOnce(Return(true)); + + MP_PERMISSIONS.take_ownership(test_path, false); +} + +TEST_F(TestPermissionUtilsDir, restrict_permissions_iterates_dir) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, true)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, take_ownership(qpath1, true)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, take_ownership(qpath2, true)).WillOnce(Return(true)); + + EXPECT_CALL(mock_platform, set_permissions(test_qpath, restricted_permissions)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, set_permissions(qpath1, restricted_permissions)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, set_permissions(qpath2, restricted_permissions)).WillOnce(Return(true)); + + MP_PERMISSIONS.restrict_permissions(test_path); +} + +struct TestPermissionUtilsBadDir : public TestPermissionUtils +{ + TestPermissionUtilsBadDir() + { + EXPECT_CALL(mock_file_ops, exists(test_path, _)).WillRepeatedly(Return(true)); + EXPECT_CALL(mock_file_ops, is_directory(test_path, _)).WillOnce(Return(true)); + + EXPECT_CALL(mock_file_ops, recursive_dir_iterator(test_path, _)).WillOnce(Return(nullptr)); + } +}; + +TEST_F(TestPermissionUtilsBadDir, set_permissions_throws_on_broken_iterator) +{ + EXPECT_CALL(mock_platform, set_permissions(test_qpath, test_permissions)).WillOnce(Return(true)); + + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.set_permissions(test_path, test_permissions), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Cannot iterate"), HasSubstr(test_path.string())))); +} + +TEST_F(TestPermissionUtilsBadDir, take_ownership_throws_on_broken_iterator) +{ + EXPECT_CALL(mock_platform, take_ownership(test_qpath, false)).WillOnce(Return(true)); + + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.take_ownership(test_path, false), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Cannot iterate"), HasSubstr(test_path.string())))); +} + +TEST_F(TestPermissionUtilsBadDir, restrict_permissions_throws_on_broken_iterator) +{ + EXPECT_CALL(mock_platform, set_permissions(test_qpath, restricted_permissions)).WillOnce(Return(true)); + EXPECT_CALL(mock_platform, take_ownership(test_qpath, true)).WillOnce(Return(true)); + + MP_EXPECT_THROW_THAT(MP_PERMISSIONS.restrict_permissions(test_path), + std::runtime_error, + mpt::match_what(AllOf(HasSubstr("Cannot iterate"), HasSubstr(test_path.string())))); +} + +} // namespace