在Python中使用特定权限写文件

56

我想创建一个只有用户可读和可写(0600)的文件。

唯一的方法是使用如下所示的 os.open() 吗?

import os
fd = os.open('/path/to/file', os.O_WRONLY, 0o600)
myFileObject = os.fdopen(fd)
myFileObject.write(...)
myFileObject.close()

理想情况下,我希望能够使用with关键字,这样我就可以自动关闭对象。是否有更好的方法来完成我上面做的事情?


如果我们使用pandas读取了CSV文件,那么如何在写入后设置权限呢? - Indrajeet Gour
6个回答

41

问题在哪里?file.close()会关闭文件,即使是使用os.open()打开的。

with os.fdopen(os.open('/path/to/file', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
  handle.write(...)

6
我认为这个回答比我的好,但它并不是“问题是什么”:你提出了一个新因素,即 OP 没有意识到的将文件处理程序转换为 Python 文件对象。请注意保持原意,使内容更加通俗易懂,不要添加解释或其他内容。 - jsbueno
@jsbueno:我刚刚将前两行合并在一起,并使用了 with。而且在问题的示例中,文件最终还是通过 myFileObject.close() 关闭的。 - vartec
1
这对我不起作用。os.open与这些标志一起使用时,需要文件已经存在。>>> f = os.open('test.txt',os.O_WRONLY,0600)跟踪(最近的调用最先): 文件“<stdin>”,第1行,<module> OSError:[Errno 2]没有这样的文件或目录:“test.txt” - Ian Goodfellow
@stair314,这可能是因为您没有指定O_CREAT。请参考答案的当前版本。 - Asclepius
2
@vartec,这个答案存在umask特定的问题。我已经发布了一个答案来解决这个问题。 - Asclepius

34
这个回答解决了vartec的答案中的多个问题,特别是umask问题。
import os
import stat

# Define file params
fname = '/tmp/myfile'
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL  # Refer to "man 2 open".
mode = stat.S_IRUSR | stat.S_IWUSR  # This is 0o600.
umask = 0o777 ^ mode  # Prevents always downgrading umask to 0.

# For security, remove file with potentially elevated mode
try:
    os.remove(fname)
except OSError:
    pass

# Open file descriptor
umask_original = os.umask(umask)
try:
    fdesc = os.open(fname, flags, mode)
finally:
    os.umask(umask_original)

# Open file handle and write to file
with os.fdopen(fdesc, 'w') as fout:
    fout.write('something\n')

如果所需模式为0600,可以更清楚地指定为八进制数0o600。更好的方法是使用stat模块。
即使首先删除了旧文件,仍然可能存在竞争条件。在标志中包括os.O_EXCLos.O_CREAT将防止由于竞争条件而创建文件。这是必要的次要安全措施,以防止打开可能已经存在具有潜在提高的mode的文件。在Python 3中,如果文件存在,则会引发FileExistsError和[Errno 17]。
未先将umask设置为00o777 ^ mode可能导致os.open设置错误的mode(权限)。这是因为默认的umask通常不是0,并且它将应用于指定的mode。例如,如果我的原始umask20o002,并且我指定的模式是0o222,如果我未先设置umask,则生成的文件实际上可能具有mode0o220,这不是我想要的。根据man 2 open,创建的文件的模式为mode & ~umaskumask尽快恢复到其原始值。此获取和设置不是线程安全的,在多线程应用程序中必须使用threading.Lock

有关umask的更多信息,请参阅此线程


1
非常感谢您解决了umask问题,已经解决了我的问题。 - Nobilis
1
感谢您的回答!我之前使用os.open(mode=0666)保存Python文件时,每个文件的权限都变成了0600。但这个解决方案解决了这个问题。 - ostrokach
1
这个答案让我有些困惑,因为它提到了异或运算。但是 umask 并不进行异或运算。然而,如果你想确保文件权限与指定的完全一致,而不会因用户的 umask 设置而导致比指定权限少的位数,将 umask 设置为 0 是正确的做法。如果你的目标仅仅是确保文件不可被其他用户读写,那么你不需要设置 umask。 - Nelson
@Nelson 已修复。已删除异或运算符。目标是通用的,即拥有一个可以使用任意指定参数权限写入文件的函数。 - Asclepius

12
更新:各位,感谢你们对我下面提出的解决方案的赞同,但是我必须反驳自己最初的提议。原因是这样做会有一定的时间,无论多么短暂,文件都存在且没有正确的权限 - 这会留下攻击的广泛方式,甚至会出现错误的行为。
当然,在第一次创建时使用正确的权限创建文件才是正确的方法,而使用Python的with只是些花头。

所以,请将这个回答视为“不要这样做”的示例;

原始帖子

您可以使用os.chmod来代替:

>>> import os
>>> name = "eek.txt"
>>> with open(name, "wt") as myfile:
...   os.chmod(name, 0o600)
...   myfile.write("eeek")
...
>>> os.system("ls -lh " + name)
-rw------- 1 gwidion gwidion 4 2011-04-11 13:47 eek.txt
0
>>>
(Note that 在 Python 中使用八进制需要显式指定前缀"0o",例如"0o600"。在 Python 2.x 版本中,写成0600也可以运行,但这两种方式都已经被弃用且易误导。)
然而,如果你的安全性很重要,最好使用os.open创建文件,并使用os.fdopenos.open返回的文件描述符获取Python文件对象。

1
我知道这不是问题的答案,但同样的代码可以用于在运行时设置文件所有者和组,只需更改os.chown(name,uid,gid)行的chmod即可。 - alemol

4
这个问题是关于如何设置权限,以确保文件不会对所有人开放只有当前用户才能读/写文件。不幸的是,仅靠以下这段代码就无法达到目的:
fd = os.open('/path/to/file', os.O_WRONLY, 0o600)

不能保证权限被拒绝给所有用户。它只会尝试为当前用户设置读写权限(如果umask允许),就是这样!

在两个非常不同的测试系统上,使用默认的umask,此代码创建一个带有-rw-r--r--的文件,并且使用umask(0)创建一个带有-rw-rw-rw-的文件,这显然不是期望的结果(并且存在严重的安全风险)。

如果你想确保文件没有设置任何组和其他用户的权限位,你必须先umask这些位(记住 - umask是权限的拒绝):

os.umask(0o177)

此外,为了100%确定文件不存在或权限不同,您需要先使用chmod/delete命令将其删除(删除更安全,因为您可能没有目标目录的写入权限 - 如果您有安全问题,则不希望在不允许写入的位置写入某个文件!),否则,如果黑客在您之前创建了具有全球读/写权限的文件以期待您的移动,您可能会遇到安全问题。在这种情况下,os.open将打开文件而不设置其权限,使您拥有一个全球读/写的秘密文件...
import os
if os.path.isfile(file):
    os.remove(file)
original_umask = os.umask(0o177)  # 0o777 ^ 0o600
try:
    handle = os.fdopen(os.open(file, os.O_WRONLY | os.O_CREAT, 0o600), 'w')
finally:
    os.umask(original_umask)

这是一种安全的方法,可以确保创建一个-rw-------文件,无论您的环境和配置如何。当然,您可以根据需要捕获并处理IOErrors。如果您没有目标目录中的写入权限,则不应该能够创建该文件,如果该文件已经存在,则删除将失败。

按照您的建议删除文件后,flags 的理想值可能是 os.O_WRONLY | os.O_CREAT | os.O_EXCL。请注意,os.O_EXCL 可以防止打开已经存在的文件。 - Asclepius
代码:“...”不能保证拒绝所有用户的权限。它只能保证当前用户对文件具有读写权限!这是错误的。提供给os.open()的权限将被设置(在掩码umask后)为创建文件时提供的权限。0o600确实意味着文件将是go-rwx。但这并不意味着文件是u+rw(因为umask)。有关详细信息,请参见open(2) man页面。可能会让您困惑的是,该模式仅适用于文件创建;如果文件已经存在,则会忽略它。 - marcelm
@marcelm 是的,你说得对(我太过于关注我的上下文,在那里我事先设置了umask),我已经修复了,谢谢! - jytou

1
我想建议对A-B-B的优秀回答进行修改,以更清晰地分离问题。 主要优点是您可以单独处理在打开文件描述符期间发生的异常和在实际写入文件期间发生的其他问题。
外部的"try ... finally"块负责处理权限和umask问题,而打开文件描述符。 内部的"with"块处理可能发生的异常,同时使用Python文件对象(因为这是OP的愿望):
try:
    oldumask = os.umask(0)
    fdesc = os.open(outfname, os.O_WRONLY | os.O_CREAT, 0o600)
    with os.fdopen(fdesc, "w") as outf:
        # ...write to outf, closes on success or on exceptions automatically...
except IOError, ... :
    # ...handle possible os.open() errors here...
finally:
    os.umask(oldumask)

如果你想追加内容到文件中而不是覆盖原有内容,那么文件描述符应该这样打开:
fdesc = os.open(outfname, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)

而且文件对象是这样的:

with os.fdopen(fdesc, "a") as outf:

当然,所有其他通常的组合都是可能的。

在这种情况下,追加现有文件是没有意义的,因为os.open指定的mode将永远不会被应用。之前存在的模式将被保留。 - Asclepius

1

我会做得不同。

from contextlib import contextmanager

@contextmanager
def umask_helper(desired_umask):
    """ A little helper to safely set and restore umask(2). """
    try:
        prev_umask = os.umask(desired_umask)
        yield
    finally:
        os.umask(prev_umask)

# ---------------------------------- […] ---------------------------------- #

        […]

        with umask_helper(0o077):
            os.mkdir(os.path.dirname(MY_FILE))
            with open(MY_FILE, 'wt') as f:
                […]


文件操作代码往往已经有了大量的try-except块;通过os.umask的finally语句使其变得更加复杂并不能让你感到更加愉悦。与此同时,自己编写上下文管理器是非常容易的,并且可以使缩进嵌套看起来更整齐。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接