返回

POSIX mode 到 Windows ACL 映射方案详解与代码示例

windows

POSIX mode 到 Windows ACL/FileAttribute 映射方案

项目要在 Windows 上实现 POSIX 层的,碰到了一个麻烦:怎么把 POSIX 的 mode(创建/创建目录时的权限位)尽可能好地对应到 Windows 的访问控制机制(ACL/FileAttribute)上?毕竟,如果完全不能兼容,就有点难办了。

目前已知的是:

  1. UCRT (Universal C Runtime) 在 mkdir 里压根不支持 mode 参数,这个参数直接从函数原型里拿掉了, 它创建目录时 ACL 参数永远是 nullptr
  2. UCRT 的 open 函数,用 SetFileAttributesA 处理 mode,但也只处理了 READ_ONLY 这一种情况。

想弄清楚有没有现成的转换例子,或者,有没有一个比较合适的方法用 ACL/FileAttributes 来实现这个映射。 下面我们仔细聊聊。

一、 问题分析

POSIX 的 mode 和 Windows 的 ACL/FileAttribute 本质上都是为了控制文件和目录的访问权限,但它们的实现机制有很大区别。

  • POSIX mode 简单直接,用几个比特位表示用户、组和其他用户的读、写、执行权限。
  • Windows ACL: 更复杂,更强大。它由一系列访问控制条目 (ACE) 组成,每个 ACE 指定一个用户或组的特定权限(读、写、执行、删除等等)。
  • Windows FileAttribute: 主要用于控制文件的一些属性, 像只读, 隐藏, 系统文件等. 和 POSIX mode 不是一个概念, 但是某些方面有功能重叠, 可以结合使用.

直接一对一映射几乎不可能。 咱们的目标是“尽力而为”,在保证核心功能的前提下,尽可能模拟 POSIX 的行为。

二、 可行方案

2.1 方案一:简化映射(UCRT 的思路)

UCRT 的做法其实提供了一个思路:只处理最基本的情况。这种方案最简单,但功能也最有限。

  • 原理:

    • mkdir:直接忽略 mode 参数,使用默认的 ACL。这意味着创建的目录会继承父目录的权限。
    • creat/open:只处理只读属性。如果 mode 包含写权限,则创建的文件可读写;否则,创建的文件只读。利用 SetFileAttributesA 实现。
  • 代码示例 (C++):

#include <Windows.h>
#include <io.h>       // For _open, _mkdir
#include <fcntl.h>    // For _O_CREAT, _O_RDWR, etc.
#include <sys/stat.h> // For _S_IREAD, _S_IWRITE

// 模拟 creat (简化版)
int my_creat(const char *pathname, int mode) {
    int flags = _O_CREAT | _O_TRUNC | _O_WRONLY;
    if ((mode & _S_IWRITE) == 0) {
        flags = _O_CREAT | _O_TRUNC | _O_RDONLY;
    }
    return _open(pathname, flags, mode); // _open 在 Windows 上可以处理创建
}
int my_mkdir(const char *dirname, int /*mode*/) { //直接忽视 mode.
  return _mkdir(dirname);
}
//open 函数处理 mode (UCRT SetFileAttributes 思路)
int my_open(const char* path_name, int flags, int mode) {
    int fd = _open(path_name, flags, mode);
    if (fd != -1 && (flags & _O_CREAT))
    {
       if ((mode & _S_IWRITE) == 0 ) {
           //使用 SetFileAttributesA 设置文件为只读。
           SetFileAttributesA(path_name, FILE_ATTRIBUTE_READONLY);
       }
    }
    return fd;
}

  • 安全性:

    • 简单映射安全性相对可控,因为基本上沿用了 Windows 默认的安全机制。
    • 需要仔细考虑是否允许创建可执行文件,防止潜在的安全风险。
  • 进阶使用:

    此方案适合对于安全性要求较高, 但是功能完整性要求较低的场景. 可以通过白名单方式仅允许创建几种特定 mode 的文件。

2.2 方案二:基于 SID 的 ACL 映射

这个方案尝试更细致地模拟 POSIX 的权限模型。核心思想是利用 Windows 的安全标识符 (SID) 来创建对应的 ACE。

  • 原理:

    1. 获取用户和组 SID: 通过 GetUserNameLookupAccountName 等 API 获取当前用户的 SID 以及相关组的 SID。
    2. 根据 mode 构建 ACE: 将 POSIX 的 mode 拆解成用户、组和其他用户的权限,然后为每个类别创建对应的 ACE。例如:
      • 如果 mode 允许用户写,就创建一个允许当前用户 SID 进行写操作的 ACE。
      • 如果 mode 允许组读,就创建一个允许当前用户主组 SID 进行读操作的 ACE。
      • 如果 mode 允许其他用户执行,就创建一个允许 "Everyone" SID 进行执行操作的 ACE (需谨慎考虑安全性!)。
    3. 创建 ACL: 将构建好的 ACE 组合成一个 ACL。
    4. 应用 ACL: 在创建文件/目录时,使用 CreateFile / CreateDirectorySECURITY_ATTRIBUTES 参数,或者通过 SetNamedSecurityInfo 应用 ACL。
  • 代码示例 (C++):

#include <Windows.h>
#include <iostream>
#include <AclAPI.h>
#include <sddl.h>  // 简化使用

bool setPermissions(const char *path, int mode) {
    // 获取用户SID, 使用 SDDL 简化表达.
    PSID pUserSid = NULL;
    ConvertStringSidToSidA("S-1-5-11", &pUserSid);  // "Authenticated Users"
                                                // 可换为 "S-1-1-0"(Everyone)
    if(!pUserSid){
        std::cerr << "Failed to Get User SID,Error:" << GetLastError()<< std::endl;
        return false;
    }
   
    //创建 DACL(discretionary access control list)
    PACL pDacl = NULL;
    EXPLICIT_ACCESS_A ea[3] = {0};

    // User Permissions
    ea[0].grfAccessPermissions = 0; // 先清 0.
    if (mode & _S_IREAD)  ea[0].grfAccessPermissions |= GENERIC_READ;
    if (mode & _S_IWRITE) ea[0].grfAccessPermissions |= GENERIC_WRITE;
    if (mode & _S_IEXEC)  ea[0].grfAccessPermissions |= GENERIC_EXECUTE;

    ea[0].grfAccessMode = SET_ACCESS;       //直接设置, 不考虑 DENY 情况。
    ea[0].grfInheritance = NO_INHERITANCE;  //简化, 不考虑继承情况.
    ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
    ea[0].Trustee.ptstrName  = (LPSTR)pUserSid;


    // Group Permissions, 简单起见, 和 User 用同一个 SID, 更严格的情况需要区分。
    ea[1] = ea[0];

    // Other persmissions. 简单处理, 全部不允许.
    ea[2].grfAccessPermissions = 0;
    ea[2].grfAccessMode = SET_ACCESS;
    ea[2].grfInheritance = NO_INHERITANCE;
    ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
    ea[2].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; // 不设置, 表示不修改.
    ea[2].Trustee.ptstrName  = nullptr;
   
    // 根据上面的设置生成 ACL.
    DWORD dwRes = SetEntriesInAclA(3, ea, NULL, &pDacl);
    if (ERROR_SUCCESS != dwRes) {
        std::cerr << "SetEntriesInAclA Error: "<< dwRes <<std::endl;
        LocalFree(pUserSid);
        return false;
    }

    //应用 ACL
    dwRes = SetNamedSecurityInfoA(
        (LPSTR)path,                 // name of the object
        SE_FILE_OBJECT,              // type of object
        DACL_SECURITY_INFORMATION,   // change only the object's DACL
        NULL, NULL,                  // do not change owner or group
        pDacl,                       // DACL specified
        NULL);                       // do not change SACL

     if (ERROR_SUCCESS != dwRes)    {
         std::cerr << "SetNamedSecurityInfo Error: "<< dwRes <<std::endl;
         LocalFree(pUserSid);
         LocalFree(pDacl);
         return false;
     }

    LocalFree(pUserSid);
    LocalFree(pDacl);
    return true;
}
// 包装 creat.
int my_creat_acl(const char *pathname, int mode)
{
     HANDLE hFile = CreateFileA(pathname,
        GENERIC_READ | GENERIC_WRITE, //需要有设置权限的权限
        0,                 // no sharing
        NULL,              // default security attributes
        CREATE_ALWAYS,     // overwrite existing
        FILE_ATTRIBUTE_NORMAL, // normal file
        NULL);

    if(hFile == INVALID_HANDLE_VALUE){
        std::cerr << "Create File Error:"<< GetLastError()<< std::endl;
         return -1;
    }

    // 创建成功, 设置 ACL。
    if (!setPermissions(pathname, mode))
    {
         CloseHandle(hFile);
         DeleteFileA(pathname);
         return -1;
    }
     //将 Handle 转换为 fd, 这里仅作示例. 需要处理各种 flag。
     int fd = _open_osfhandle((intptr_t)hFile, _O_RDWR);
     return fd;
}

  • 安全性:

    • 这种方案比简化映射更灵活,但也更容易出错。
    • 强烈建议不要轻易给 "Everyone" 或其他广泛的用户组授予写权限或执行权限。
    • 仔细处理 SID 的获取和使用,防止权限提升漏洞。
  • 进阶使用

    可以增加 Owner/Group 映射功能, 让权限控制更加精细, 可以自定义安全符定义语言 (SDDL) 字符串,实现更复杂的 ACE 配置。 可以研究ConvertStringSecurityDescriptorToSecurityDescriptor 等函数

2.3 方案三: 混合使用 FileAttribute 与 ACL

可以在方案二的基础上,进一步结合 FileAttribute。 例如:

  1. POSIX 只读位(S_IREAD没有 S_IWRITE) , 可以通过设置FILE_ATTRIBUTE_READONLY来实现, 简化 ACL 设置的复杂性.
  2. 可执行文件属性, 可以根据实际需要通过SetFileAttributes 或者 ACE 来控制。

这样可以充分利用两种机制, 实现最好的映射效果。

  • 代码:
    结合上述方案二的代码 和 方案一中 SetFileAttributes 的部分. 根据具体情况进行调整。
    这种方式更灵活. 需要做好错误处理。

  • 安全: 与方案二类似。

  • 进阶技巧: 可以进一步探索Windows的安全模型. 结合 mandatory integrity control (MIC) 等更高级的功能来实现安全控制.

三、 总结

上面提出了几种映射 POSIX mode 到 Windows ACL/FileAttribute 的方案,各有优缺点。

  • 方案一 (简化映射) 简单, 安全, 但功能受限.
  • 方案二 (基于 SID 的 ACL 映射) 更接近 POSIX 模型, 但复杂, 需要仔细考虑安全性.
  • 方案三 (混合使用) 最灵活, 但是需要对两种机制都有一定了解。

实际开发中,要根据具体需求, 进行取舍, 选择一个合适的方案, 或者几个方案混合使用。 要记住,安全第一!