春江暮客

春江暮客的个人学习分享网站

Python 新建文件权限排查:umask、600 与拒绝覆盖

2026-10-28 技术
Python 新建文件权限排查:umask、600 与拒绝覆盖

Python 脚本导出的报表只打算自己看,换个终端或放进自动化流程后,文件却变成了 644。这通常与创建时请求的权限和进程继承的 umask 有关,不是 Python 随机改变了权限。

本文面向初学者到中级读者:先复现 022 与 077 的差别,再用标准库让一个新 JSON 文件从创建时就不开放组和其他用户的权限。演示只使用 ready 这样的假数据,不读取真实凭据,也不修改服务器或调度设置。

1. 方法:区分整次任务与单个文件

  • **整次任务使用较紧的默认权限:**在子 shell 中设置 umask 077,不改变当前终端的设置。
  • **只限制一个新文件:**通过 open() 的 opener 请求 0o600,并用 x 模式拒绝覆盖。下文提供完整脚本。

没有目录默认 ACL 时,新建文件的权限可按 requested_mode & ~umask 理解;不是十进制减法。普通文本创建通常请求 666,因此 022 得到 644,077 得到 600。600 表示所有者可读写,组和其他用户没有权限;它不代表加密。Linux umask 手册

权限控制不替代目录隔离、备份与访问管理。示例假定目录及其父目录可信、没有其他任务同时替换路径;不要在共享可写目录中处理真实敏感数据。

2. 准备环境与复现差异

需要 Linux、已有的 Python 3.9 或更新版本,无第三方依赖。命令适用于 bash 或 zsh,已在 Linux、Python 3.11.4 测试。保持在同一个终端、同一个目录操作;重做实验时换一个新目录名。

  1. 查看版本和当前 shell 的掩码;单独运行 umask 只显示当前值:
python3 --version
mkdir umask-demo
cd umask-demo
umask
  1. 在两个子 shell 中分别创建文件,再查看实际权限:
(
  umask 022
  python3 -c "with open('ordinary-022.txt', 'x', encoding='utf-8') as f: f.write('demo\n')"
)
(
  umask 077
  python3 -c "with open('ordinary-077.txt', 'x', encoding='utf-8') as f: f.write('demo\n')"
)
python3 - <<'PY_MODES'
from pathlib import Path
import stat
for name in ("ordinary-022.txt", "ordinary-077.txt"):
    mode = stat.S_IMODE(Path(name).stat().st_mode)
    print(f"{name}: {mode:03o}")
PY_MODES

在无默认 ACL 的普通本地目录中,预期输出:

ordinary-022.txt: 644
ordinary-077.txt: 600

括号中的设置在子 shell 结束后失效。如果输出不同,先检查父目录的默认 ACL 和文件系统语义,不要直接把整个目录 chmod -R。默认 ACL 会改变创建权限的计算方式,见前面的 Linux 手册。

3. 完整实现:只创建,不覆盖

把以下代码保存为当前目录的 write_private.py:

"""Create a new private demo JSON file on Linux. Python 3.9+, standard library."""
import argparse
import json
import os
from pathlib import Path
import stat
import sys


def private_opener(path, flags):
    return os.open(path, flags, 0o600)


def main():
    parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
    parser.add_argument("output", type=Path, help="new file in a trusted directory")
    args = parser.parse_args()
    try:
        with open(args.output, "x", encoding="utf-8", newline="\n",
                  opener=private_opener) as handle:
            json.dump({"status": "ready"}, handle)
            handle.write("\n")
            mode = stat.S_IMODE(os.fstat(handle.fileno()).st_mode)
    except FileExistsError:
        print("ERROR: output already exists; not changed", file=sys.stderr)
        return 1
    except OSError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1
    print(json.dumps({"file": str(args.output), "mode": f"{mode:03o}"}))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

open() 的 opener 参数接收路径和打开标志,返回文件描述符。本例原样保留 x 模式提供的标志,只把底层 os.open() 的创建权限设为 0o600。掩码仍可能继续收紧所有者权限,因此结果是 600 或更严格,不是强行保证可读写。

POSIX 的排他创建会拒绝已有路径,包括最终路径上的符号链接;但它不保护可被替换的父目录。Linux open 手册

代码不调用 os.umask(),避免临时改变整个进程的创建策略。如果你的问题是目录根本无法写入,先用目录写权限探测定位原因;收紧权限不会让不可写目录变得可写。

4. 运行:宽松掩码下也只请求私有权限

  1. 查看帮助,并在 umask 000 的子 shell 中运行。随后重复相同路径,确认拒绝覆盖:
python3 write_private.py --help
(
  umask 000
  python3 write_private.py private.json
)
python3 -m json.tool private.json
if python3 write_private.py private.json; then
  printf 'Unexpected success\n'
else
  printf 'Existing file refused\n'
fi

首次运行的标准输出为:

{"file": "private.json", "mode": "600"}

json.tool 应显示 {"status": "ready"} 对应的格式化内容。第二次运行向 stderr 输出 ERROR: output already exists; not changed,shell 随后输出 Existing file refused。

  1. 调用方按退出码处理结果:0 表示创建、写入和关闭成功;1 表示已有路径或其他文件操作失败;2 表示命令参数错误。只有成功时 stdout 才有一行包含路径和权限的 JSON,不输出文件正文。

**这不是原子发布实现。**写入或关闭失败时,已经创建的文件可能为空或不完整,调用方必须处理非零退出码,不能把存在当作成功。需要更新已有结果时,继续阅读原子保存 JSON,并在替换前明确新文件的权限。

5. 验证:不同掩码和拒绝覆盖分支

下面的自检在独立临时子目录里执行,不接触刚才的文件。测试中的 umask= 只设置子进程,参数说明见 Python subprocess 文档。

python3 - <<'PY_CHECK'
import json
from pathlib import Path
import stat
import subprocess
import sys
import tempfile

script = Path("write_private.py").resolve()
with tempfile.TemporaryDirectory(prefix="mode-check-", dir=".") as folder:
    root = Path(folder)
    def run(path, mask=0o022):
        return subprocess.run(
            [sys.executable, str(script), str(path)],
            umask=mask, capture_output=True, text=True, check=False,
        )

    for mask, expected in [(0o000, 0o600), (0o022, 0o600),
                           (0o077, 0o600), (0o277, 0o400)]:
        path = root / f"private-{mask:03o}.json"
        result = run(path, mask)
        assert result.returncode == 0 and not result.stderr, result
        assert stat.S_IMODE(path.stat().st_mode) == expected
        assert json.loads(result.stdout)["mode"] == f"{expected:03o}"
        assert json.loads(path.read_text()) == {"status": "ready"}

    existing = root / "existing.json"
    existing.write_bytes(b"keep\n")
    existing.chmod(0o644)
    link = root / "link.json"
    link.symlink_to(existing.name)
    broken = root / "broken.json"
    broken.symlink_to("absent.json")
    for path in (existing, link, broken, root, root / "missing" / "file.json"):
        result = run(path)
        assert result.returncode == 1 and result.stdout == "", result
        assert result.stderr.startswith("ERROR:"), result
    assert existing.read_bytes() == b"keep\n"
    assert stat.S_IMODE(existing.stat().st_mode) == 0o644
    assert link.is_symlink() and broken.is_symlink()
    assert not (root / "absent.json").exists()
    result = subprocess.run([sys.executable, str(script)],
                            capture_output=True, text=True, check=False)
    assert result.returncode == 2 and not result.stdout and result.stderr
print("OK: masks, JSON, existing file, symlinks, directory, missing parent, arguments")
PY_CHECK

预期输出:

OK: masks, JSON, existing file, symlinks, directory, missing parent, arguments

测试同时检查内容和权限,并验证已有文件的字节与 644 权限均未被修改。277 还会移除所有者写权限,结果为 400;创建时打开的写描述符仍可完成这次写入,后续新打开则受文件权限限制。临时目录随测试结束清理;更多用法见临时目录自动清理。

6. 排错:不要用放宽权限掩盖原因

现象 具体处理
改为 077 后旧文件仍是 644 掩码只影响新建。先核对文件所有者和共享需求,再对具体旧文件收紧权限;不要递归修改整棵目录。
output already exists 选择新的输出名,或确认旧结果后设计明确的替换流程;不要删掉旧文件只为让自检通过。
Permission denied 检查执行账号、父目录访问权限与实际文件权限,按前文的目录探测教程排查,不要直接使用 777。
No such file or directory 本例不自动建父目录。先在可信位置创建输出目录,再传入完整路径。
观察结果不同于权限表 核对默认 ACL、挂载和文件系统权限支持;先在普通本地演示目录复现。
Web 服务器读不到输出 私有文件与要公开的 Hugo 静态资源需要不同策略。只对确定应公开的产物设置必要读权限,勿把敏感文件放入站点目录。

只对本文自己创建的演示文件,演示一次旧文件修复:

python3 - <<'PY_FIX'
from pathlib import Path
import stat
path = Path("ordinary-022.txt")
path.chmod(0o600)
print(f"{path.name}: {stat.S_IMODE(path.stat().st_mode):03o}")
PY_FIX

预期 ordinary-022.txt: 600。这个修改不受创建掩码控制。若真实数据曾以较宽权限存在,事后收紧权限不能收回已经被读取或复制的内容。

7. 小结

整次私有任务可以用子 shell 的 umask 077 限制默认权限;单个输出则在排他创建时直接请求 0o600。先验证新建权限、拒绝覆盖和退出码,再将同样的检查放进实际流程,别把权限正确误当成内容完整。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 etag evaluation faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io file-management files flashppi flask folium frontend game generator git github-actions google google-research gpt-6.1-sol grep gzip harness heapq hls html http hugo imports indexnow jailbreak javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance permissions phaser pillow pip postfix preprocessing print prompt-engineering protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sitemap sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv vhhbert vite webp wordcloud wordpress workflow xml yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它