Acl tests 38/319038/10
authorKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Mon, 3 Feb 2025 15:53:25 +0000 (16:53 +0100)
committerKrzysztof Malysa <k.malysa@samsung.com>
Wed, 12 Feb 2025 17:03:10 +0000 (18:03 +0100)
Change-Id: Ic0f8bcf612681bba6f088d48083d68ff1f2fe8dd

test/CMakeLists.txt
test/test_acl.cpp [new file with mode: 0644]

index f2f9ae0dfa4b56f21bea79e2e0e7afd1ebdb1ab1..6f18f6dc57104e78a0c491700539d01b6cf8f333 100644 (file)
@@ -20,6 +20,7 @@
 #
 
 PKG_CHECK_MODULES(COMMON_DEP REQUIRED
+    libacl
     libtzplatform-config
     libsessiond
     libsystemd
@@ -79,6 +80,7 @@ SET(SM_TESTS_SOURCES
     ${SM_TEST_SRC}/colour_log_formatter.cpp
     ${SM_TEST_SRC}/privilege_db_fixture.cpp
     ${SM_TEST_SRC}/security-manager-tests.cpp
+    ${SM_TEST_SRC}/test_acl.cpp
     ${SM_TEST_SRC}/test_log.cpp
     ${SM_TEST_SRC}/test_filesystem.cpp
     ${SM_TEST_SRC}/test_file-lock.cpp
@@ -109,6 +111,7 @@ SET(SM_TESTS_SOURCES
     ${DPL_PATH}/log/src/log.cpp
     ${DPL_PATH}/log/src/old_style_log_provider.cpp
     ${DPL_PATH}/log/src/sd_journal_provider.cpp
+    ${PROJECT_SOURCE_DIR}/src/common/acl.cpp
     ${PROJECT_SOURCE_DIR}/src/common/check-proper-drop.cpp
     ${PROJECT_SOURCE_DIR}/src/common/config-file.cpp
     ${PROJECT_SOURCE_DIR}/src/common/credentials.cpp
diff --git a/test/test_acl.cpp b/test/test_acl.cpp
new file mode 100644 (file)
index 0000000..8b74f15
--- /dev/null
@@ -0,0 +1,629 @@
+/*
+ * 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()