--- /dev/null
+/*
+ * Copyright (c) 2025 Samsung Electronics Co., Ltd. All rights reserved.
+ *
+ * This file is licensed under the terms of MIT License or the Apache License
+ * Version 2.0 of your choice. See the LICENSE.MIT file for MIT license details.
+ * See the LICENSE file or the notice below for Apache License Version 2.0
+ * details.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <acl/libacl.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <cstdint>
+#include <unordered_set>
+#include <optional>
+
+#include <testmacros.h>
+
+#include "acl.h"
+
+using namespace SecurityManager;
+
+namespace {
+
+template<size_t N>
+inline constexpr uint8_t bitsNeeded = [] {
+ uint8_t res = 0;
+ auto n = N;
+ while (n > 0) {
+ ++res;
+ n >>= 1;
+ }
+ return res;
+}();
+
+template<auto... N>
+inline constexpr auto max = [] {
+ auto vals = {N...};
+ auto res = *vals.begin();
+ for (auto x : vals)
+ if (x > res)
+ res = x;
+ return res;
+}();
+
+inline constexpr size_t MAX_TAG =
+ max<ACL_USER_OBJ, ACL_USER, ACL_GROUP_OBJ, ACL_GROUP, ACL_MASK, ACL_OTHER>;
+inline constexpr size_t MAX_PERM = ACL_READ | ACL_WRITE | ACL_EXECUTE;
+
+
+class AclHelper
+{
+public:
+ struct AclEntry
+ {
+ acl_tag_t tag = ACL_UNDEFINED_TAG;
+ std::optional<unsigned int> qualifier = std::nullopt;
+ acl_perm_t perms = 0;
+ };
+
+ struct Hash
+ {
+ size_t operator()(const AclEntry &entry) const
+ {
+ static constexpr auto TAG_BITS = bitsNeeded<MAX_TAG>;
+ static constexpr auto PERM_BITS = bitsNeeded<MAX_PERM>;
+ size_t hash = 0;
+ BOOST_REQUIRE_GE(entry.tag, 0);
+ BOOST_REQUIRE_LE(static_cast<size_t>(entry.tag), MAX_TAG);
+ hash |= static_cast<size_t>(entry.tag);
+ BOOST_REQUIRE_LE(entry.perms, MAX_PERM);
+ hash |= static_cast<size_t>(entry.perms << TAG_BITS);
+ if (entry.qualifier.has_value()) {
+ BOOST_REQUIRE_LE(*entry.qualifier,
+ std::numeric_limits<decltype(hash)>::max() >>
+ (TAG_BITS + PERM_BITS));
+ hash |= *entry.qualifier << (TAG_BITS + PERM_BITS);
+ }
+ return hash;
+ }
+ };
+
+ explicit AclHelper(acl_t&& acl)
+ {
+ BOOST_REQUIRE_NE(acl, nullptr);
+
+ acl_entry_t entry;
+ int ret = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry);
+ BOOST_REQUIRE(ret >= 0);
+ while (ret == 1) {
+ AclEntry aclEntry;
+ BOOST_REQUIRE_EQUAL(0, acl_get_tag_type(entry, &aclEntry.tag));
+ if (aclEntry.tag == ACL_USER || aclEntry.tag == ACL_GROUP) {
+ auto q = acl_get_qualifier(entry);
+ BOOST_REQUIRE(q != nullptr);
+ static_assert(std::is_same_v<uid_t, unsigned int>);
+ static_assert(std::is_same_v<gid_t, unsigned int>);
+ aclEntry.qualifier = *static_cast<unsigned int*>(q);
+ }
+ acl_permset_t permset;
+ BOOST_REQUIRE_EQUAL(0, acl_get_permset(entry, &permset));
+
+ for (auto perm : { ACL_READ, ACL_WRITE, ACL_EXECUTE }) {
+ ret = acl_get_perm(permset, perm);
+ BOOST_REQUIRE(ret >= 0);
+
+ if (ret == 1)
+ aclEntry.perms |= perm;
+ }
+ m_entries.emplace(std::move(aclEntry));
+
+ ret = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry);
+ BOOST_REQUIRE(ret >= 0);
+ }
+ acl_free(acl);
+ }
+
+ friend bool operator==(const AclHelper &first, const AclHelper &second) noexcept;
+
+ friend bool operator!=(const AclHelper &first, const AclHelper &second) noexcept
+ {
+ return !(first == second);
+ }
+
+ friend std::ostream& operator<<(std::ostream &os, const AclHelper &acl);
+
+private:
+ std::unordered_set<AclEntry, Hash> m_entries;
+};
+
+constexpr bool operator==(const AclHelper::AclEntry &first, const AclHelper::AclEntry &second) noexcept
+{
+ return first.tag == second.tag && first.perms == second.perms &&
+ first.qualifier == second.qualifier;
+}
+
+bool operator==(const AclHelper &first, const AclHelper &second) noexcept
+{
+ return first.m_entries == second.m_entries;
+}
+
+std::ostream& operator<<(std::ostream &os, const AclHelper::AclEntry &entry)
+{
+ switch (entry.tag) {
+ case ACL_UNDEFINED_TAG:
+ os << "UNDEFINED_TAG|";
+ break;
+ case ACL_USER_OBJ:
+ os << "USER_OBJ|";
+ break;
+ case ACL_USER:
+ os << "USER|";
+ break;
+ case ACL_GROUP_OBJ:
+ os << "GROUP_OBJ|";
+ break;
+ case ACL_GROUP:
+ os << "GROUP|";
+ break;
+ case ACL_MASK:
+ os << "MASK|";
+ break;
+ case ACL_OTHER:
+ os << "OTHER|";
+ break;
+ default:
+ BOOST_FAIL("Unknown tag");
+ }
+
+ if (entry.qualifier.has_value())
+ os << *entry.qualifier << "|";
+
+ if (entry.perms & Acl::R)
+ os << "r";
+ else
+ os << "-";
+ if (entry.perms & Acl::W)
+ os << "w";
+ else
+ os << "-";
+ if (entry.perms & Acl::X)
+ os << "x";
+ else
+ os << "-";
+ return os;
+}
+
+std::ostream& operator<<(std::ostream &os, const AclHelper &acl)
+{
+ os << "{\n";
+ for (const auto &entry : acl.m_entries)
+ os << " " << entry << "\n";
+ return os << "}";
+}
+
+template<typename T>
+void BOOST_REQUIRE_EQUAL_MSG(const T& expected, const T& got, const std::string& msg)
+{
+ if (expected != got) {
+ BOOST_TEST_MESSAGE(msg);
+ BOOST_REQUIRE_EQUAL(expected, got);
+ }
+}
+
+template<typename T>
+void CheckAcl(const T &obj, const std::string &aclText, acl_type_t type)
+{
+ AclHelper got(acl_get_file(obj.Path().c_str(), type));
+ AclHelper expected(acl_from_text(aclText.c_str()));
+ BOOST_REQUIRE_EQUAL_MSG(expected, got,
+ std::string("Checking ") + (type == ACL_TYPE_ACCESS ? "access" : "default") +
+ " ACL for: " + obj.Path());
+}
+
+template<typename T>
+void CheckAccess(const T &obj, const std::string &aclText)
+{
+ CheckAcl(obj, aclText, ACL_TYPE_ACCESS);
+}
+
+template<typename T>
+void CheckDefault(const T &obj, const std::string &aclText)
+{
+ CheckAcl(obj, aclText, ACL_TYPE_DEFAULT);
+}
+
+class Dir
+{
+public:
+ Dir(const std::string &path, mode_t mode) : m_path(path)
+ {
+ BOOST_REQUIRE_EQUAL(0, mkdir(path.c_str(), mode));
+ }
+
+ ~Dir()
+ {
+ rmdir(m_path.c_str());
+ }
+
+ const std::string& Path() const
+ {
+ return m_path;
+ }
+
+private:
+ std::string m_path;
+};
+
+class File
+{
+public:
+ File(const std::string &path, mode_t mode) :
+ m_path(path)
+ {
+ int fd = creat(path.c_str(), mode);
+ BOOST_REQUIRE(fd >= 0);
+ close(fd);
+ }
+
+ ~File()
+ {
+ unlink(m_path.c_str());
+ }
+
+ const std::string& Path() const
+ {
+ return m_path;
+ }
+
+private:
+ std::string m_path;
+};
+
+template<typename T>
+void CheckMode(const T &obj, mode_t mode)
+{
+ struct stat statbuf;
+ BOOST_REQUIRE_EQUAL(0, stat(obj.Path().c_str(), &statbuf));
+ BOOST_REQUIRE_EQUAL_MSG(mode, statbuf.st_mode & 07777,
+ std::string("Checking permissions for: ") + obj.Path());
+}
+
+const std::string TEST_DIR = "/tmp/security_manager_unit_tests_dir";
+const std::string TEST_FILE = "/tmp/security_manager_unit_tests_file";
+const std::string TEST_SUBDIR = TEST_DIR + "/subdir";
+const std::string TEST_SUBFILE = TEST_DIR + "/subfile";
+const std::string TEST_SUBFILE2 = TEST_DIR + "/subfile2";
+const std::string TEST_SUBSUBFILE = TEST_SUBDIR + "/subfile";
+
+mode_t GetUmask()
+{
+ auto currentUmask = umask(777);
+ umask(currentUmask);
+ return currentUmask;
+}
+
+template <typename T>
+void Chmod(const T& obj, mode_t mode)
+{
+ BOOST_REQUIRE_EQUAL(0, chmod(obj.Path().c_str(), mode));
+}
+
+std::string ToAclString(mode_t mode, const std::optional<std::string>& lastGroupObj = std::nullopt)
+{
+ auto toStr = [](mode_t m) {
+ m &= 7;
+ if (m == 0)
+ return std::string("-");
+
+ std::string ret;
+ if (m & 4)
+ ret += "r";
+ if (m & 2)
+ ret += "w";
+ if (m & 1)
+ ret += "x";
+ return ret;
+ };
+ // Adjust it to acl_get_file() which returns optional empty ACL_MASK. However, if ACL_MASK is
+ // present it corresponds to (equals) DAC group permissions. If permissions were modified using
+ // ACL, the ACL_MASK will be equal to ACL_GROUP_OBJ. If permissions were modified using chmod()
+ // the ACL_GROUP_OBJ will be unchanged.
+ return std::string("u::") + toStr(mode >> 6) +
+ ",g::" + (lastGroupObj.has_value() ? *lastGroupObj : toStr(mode >> 3)) +
+ ",o::" + toStr(mode) +
+ ",m::" + toStr(mode >> 3);
+}
+
+Acl ToAcl(mode_t mode)
+{
+ auto toPerm = [](mode_t m) {
+ acl_perm_t perm = 0;
+
+ if (m & 4)
+ perm |= ACL_READ;
+ if (m & 2)
+ perm |= ACL_WRITE;
+ if (m & 1)
+ perm |= ACL_EXECUTE;
+ return perm;
+ };
+ return Acl::Make(toPerm(mode >> 6), toPerm(mode >> 3), toPerm(mode));
+}
+
+} // namespace
+
+BOOST_AUTO_TEST_SUITE (ACL_TEST)
+
+POSITIVE_TEST_CASE(T1800_access_basic)
+{
+ mode_t mode = 0000;
+ Dir dir(TEST_DIR, mode);
+ File file(TEST_FILE, mode);
+
+ auto acl = Acl::Make(0, 0, 0);
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+ BOOST_REQUIRE_NO_THROW(acl.apply(file.Path(), ACL_TYPE_ACCESS));
+
+ CheckAccess(dir, "u::-,g::-,o::-,m::-");
+ CheckAccess(file, "u::-,g::-,o::-,m::-");
+
+ CheckMode(dir, mode);
+ CheckMode(file, mode);
+}
+
+POSITIVE_TEST_CASE(T1810_default_basic)
+{
+ mode_t mode = 0000;
+ Dir dir(TEST_DIR, mode);
+
+ auto acl = Acl::Make(0, 0, 0);
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_DEFAULT));
+
+ CheckDefault(dir, "u::-,g::-,o::-,m::-");
+
+ CheckMode(dir, mode);
+}
+
+POSITIVE_TEST_CASE(T1820_access_entries)
+{
+ mode_t mode = 0000;
+ Dir dir(TEST_DIR, mode);
+ File file(TEST_FILE, mode);
+
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::RWX, 5001));
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::RX, 100));
+
+ auto acl = Acl::Make(0, 0, 0, std::move(entries));
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+ BOOST_REQUIRE_NO_THROW(acl.apply(file.Path(), ACL_TYPE_ACCESS));
+
+ CheckAccess(dir, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rwx");
+ CheckAccess(file, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rwx");
+
+ // group gets the ACL_MASK which is an upper limit for ACL_USER and ACL_GROUP (see man 5 acl)
+ CheckMode(dir, mode | 0070);
+ CheckMode(file, mode | 0070);
+}
+
+POSITIVE_TEST_CASE(T1830_default_entries)
+{
+ mode_t mode = 0000;
+ Dir dir(TEST_DIR, mode);
+
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::RWX, 5001));
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::RX, 100));
+
+ auto acl = Acl::Make(0, 0, 0, std::move(entries));
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_DEFAULT));
+
+ CheckDefault(dir, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rwx");
+
+ CheckMode(dir, mode);
+}
+
+NEGATIVE_TEST_CASE(T1840_malformed)
+{
+ File file(TEST_FILE, 0000);
+ Acl acl;
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), ACL_TYPE_ACCESS), Acl::Exception::Base);
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), ACL_TYPE_DEFAULT), Acl::Exception::Base);
+}
+
+NEGATIVE_TEST_CASE(T1850_no_file)
+{
+ auto acl = Acl::Make(0, 0, 0);
+
+ BOOST_REQUIRE_THROW(acl.apply(TEST_FILE, ACL_TYPE_DEFAULT), Acl::Exception::Base);
+ BOOST_REQUIRE_THROW(acl.apply(TEST_FILE, ACL_TYPE_DEFAULT), Acl::Exception::Base);
+}
+
+NEGATIVE_TEST_CASE(T1860_wrong_type)
+{
+ File file(TEST_FILE, 0000);
+
+ auto acl = Acl::Make(0, 0, 0);
+
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), 0), Acl::Exception::Base);
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), 1), Acl::Exception::Base);
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), 0xFFFF), Acl::Exception::Base);
+}
+
+NEGATIVE_TEST_CASE(T1870_default_on_file)
+{
+ File file(TEST_FILE, 0000);
+
+ auto acl = Acl::Make(0, 0, 0);
+
+ BOOST_REQUIRE_THROW(acl.apply(file.Path(), ACL_TYPE_DEFAULT), Acl::Exception::Base);
+}
+
+NEGATIVE_TEST_CASE(T1880_duplicated_entries)
+{
+ Dir dir(TEST_DIR, 0000);
+
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::RWX, 5001));
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::R, 5001));
+
+ BOOST_REQUIRE_THROW(Acl::Make(0, 0, 0, std::move(entries)), Acl::Exception::Base);
+
+ entries = std::vector<Acl::EntryPtr>{};
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::RWX, 100));
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::R, 100));
+
+ BOOST_REQUIRE_THROW(Acl::Make(0, 0, 0, std::move(entries)), Acl::Exception::Base);
+}
+
+POSITIVE_TEST_CASE(T1890_default_file_creation)
+{
+ mode_t mode = 0060; // '6' corresponds with ACL_MASK so make it identical (see man 5 acl)
+ Dir dir(TEST_DIR, mode);
+
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::RW, 5001));
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::R, 100));
+
+ auto acl = Acl::Make(0, 0, 0, std::move(entries));
+
+ static constexpr char EXPECTED_ACL[] = "u::-,g::-,o::-,u:5001:rw,g:100:r,m::rw";
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_DEFAULT));
+ CheckDefault(dir, EXPECTED_ACL);
+
+ // in the absence of ACL the effective mode is affected by umask (see man creat, man umask)
+ CheckMode(dir, mode & ~GetUmask());
+
+ File subfile(TEST_SUBFILE, mode);
+ Dir subdir(TEST_SUBDIR, mode);
+ File subsubfile(TEST_SUBSUBFILE, mode);
+
+ CheckAccess(subfile, EXPECTED_ACL);
+ CheckAccess(subdir, EXPECTED_ACL);
+ CheckAccess(subsubfile, EXPECTED_ACL);
+
+ CheckDefault(subdir, EXPECTED_ACL); // effective default ACL from parent directory is returned
+
+ CheckMode(subfile, mode);
+ CheckMode(subdir, mode);
+ CheckMode(subsubfile, mode);
+}
+
+POSITIVE_TEST_CASE(T1900_file_permissions_correspondence)
+{
+ mode_t mode = 0750;
+ Dir dir(TEST_DIR, mode);
+
+ mode = 0040;
+ auto acl = ToAcl(mode);
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+ CheckMode(dir, mode);
+ CheckAccess(dir, ToAclString(mode));
+
+ mode = 0777;
+ Chmod(dir, mode);
+ CheckMode(dir, mode);
+ CheckAccess(dir, ToAclString(mode, "r"));
+
+ mode = 0111;
+ Chmod(dir, mode);
+ CheckMode(dir, mode);
+ CheckAccess(dir, ToAclString(mode, "r"));
+
+ mode = 0123;
+ acl = ToAcl(mode);
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+ CheckMode(dir, mode);
+ CheckAccess(dir, ToAclString(mode));
+
+ mode = 0765;
+ acl = ToAcl(mode);
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+ CheckMode(dir, mode);
+ CheckAccess(dir, ToAclString(mode));
+}
+
+POSITIVE_TEST_CASE(T1910_mask)
+{
+ mode_t mode = 0010;
+ Dir dir(TEST_DIR, mode);
+ CheckAccess(dir, "u::-,g::x,o::-"); // no ACL yet -> no mask
+
+ // change the ACL_MASK using ACL_GROUP entry
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::R, 100));
+
+ auto acl = Acl::Make(0, Acl::X, 0, std::move(entries));
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+
+ CheckAccess(dir, "u::-,g::x,o::-,g:100:r,m::rx");
+ CheckMode(dir, 0050); // ACL_MASK affects group permission
+
+ // change the ACL_MASK using ACL_USER entry
+ std::vector<Acl::EntryPtr> entries2;
+ entries2.emplace_back(std::make_unique<Acl::GidEntry>(Acl::R, 100));
+ entries2.emplace_back(std::make_unique<Acl::UidEntry>(Acl::W, 5001));
+ acl = Acl::Make(0, Acl::X, 0, std::move(entries2));
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+
+ CheckAccess(dir, "u::-,g::x,o::-,u:5001:w,g:100:r,m::rwx");
+ CheckMode(dir, 0070); // ACL_MASK affects group permission
+
+ // reset the ACL_MASK
+ acl = Acl::Make(0, Acl::X, 0);
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_ACCESS));
+
+ CheckAccess(dir, "u::-,g::x,o::-,m::x");
+ CheckMode(dir, 0010); // ACL_MASK affects group permission
+
+ // change the ACL_MASK (but not ACL_GROUP_OBJ) using chmod
+ Chmod(dir, 0020);
+ CheckAccess(dir, "u::-,g::x,o::-,m::w");
+}
+
+POSITIVE_TEST_CASE(T1920_executable_permission)
+{
+ mode_t mode = 0750;
+ Dir dir(TEST_DIR, mode);
+
+ std::vector<Acl::EntryPtr> entries;
+ entries.emplace_back(std::make_unique<Acl::UidEntry>(Acl::RWX, 5001));
+ entries.emplace_back(std::make_unique<Acl::GidEntry>(Acl::RX, 100));
+
+ auto acl = Acl::Make(0, 0, 0, std::move(entries));
+
+ BOOST_REQUIRE_NO_THROW(acl.apply(dir.Path(), ACL_TYPE_DEFAULT));
+ CheckDefault(dir, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rwx");
+
+ // executable
+ File file(TEST_SUBFILE, 0750);
+ CheckAccess(file, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rx"); // file group mode affects mask
+
+ // regular file
+ File file2(TEST_SUBFILE2, 0640);
+ // u:5001 has 'rwx' and g:100 has 'rx' but effectively it's limited to r by the mask
+ CheckAccess(file2, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::r");
+
+ // subdir
+ File subdir(TEST_SUBDIR, 0750);
+ CheckAccess(subdir, "u::-,g::-,o::-,u:5001:rwx,g:100:rx,m::rx");
+}
+
+// TODO
+// - setgid/setuid
+// - effective permissions
+// - umask interaction
+// - nested default ACL
+
+BOOST_AUTO_TEST_SUITE_END()