اولین باری که به این خطا برخوردم، تازه کار حرفه‌ای خود را شروع کرده بودم و ساعت‌ها در منطق کد گشتم؛ در حالی که یک فایل لاگ با مالکیت root ساخته شده بود و اسکریپت با کاربر عادی اجرا می‌شد. آن روز یاد گرفتم همیشه پیش از دست‌زدن به منطق برنامه، ابتدا کاربر اجراکننده و وضعیت مجوز فایل را بررسی کنم. این مقاله حاصل همان تجربه و ده‌ها پروژه بعدی است؛ از اسکریپت‌های ساده‌ی آموزش پایتون از صفر تا سرویس‌های سنگین Django که در آن‌ها PermissionError (خطای دسترسی) نقشی محوری داشت.

PermissionError چیست و از کجا می‌آید؟

PermissionError یکی از زیرکلاس‌های OSError در پایتون است که وقتی پرتاب می‌شود که سیستمعامل به فرآیند پایتونی، اجازهٔ انجام یک عملیات روی یک منبع (فایل، دایرکتوری، سوکت دامنه‌ای، یا حتی گاهی یک دستگاه ورودی/خروجی) را ندهد. این خطا مستقیماً از هستهٔ سیستمعامل می‌آید؛ یعنی خود پایتون آن را «تولید» نمی‌کند، بلکه لایهٔ واسط (CPython) آن را از errno دریافت و به یک استثنای سطح بالا ترجمه می‌کند.

از منظر کلاس‌بندی، ساختار ارث‌بری به این شکل است:

BaseException
 └── Exception
      └── OSError
           ├── FileNotFoundError
           ├── PermissionError
           ├── FileExistsError
           ├── IsADirectoryError
           ├── NotADirectoryError
           ├── ConnectionError
           └── ... (سایر خطاهای سیستمعامل)

این یعنی اگر کدی بنویسید که except OSError را بگیرد، به‌طور خودکار PermissionError و FileNotFoundError را هم می‌گیرد. همین ویژگی در سناریوهای دیباگ بسیار کارساز است، چون گاهی نمی‌دانید دقیقاً کدام‌یک از خطاهای خانوادهٔ OSError رخ می‌دهد؛ گرفتن والد، بخش بزرگی از ابهام را حذف می‌کند. برای درک عمیق‌تر این سلسله‌مراتب، مقالهٔ مدیریت خطا در پایتون را پیشنهاد می‌کنم.

PermissionError یک «باگ برنامه» نیست؛ یک «واقعیت سیستمعامل» است. تا زمانی که این تفاوت را نپذیرید، هر دیباگی را در منطق کد می‌جویید و بی‌نتیجه می‌مانید.

نکتهٔ ظریف این‌جاست: پایتون هنگام برخورد با خطای مجوز، errno را هم درون شیء استثنا نگه می‌دارد. مثلاً errno.EACCES (کد ۱۳ در لینوکس) دقیقاً یعنی «دسترسی رد شد». با نگاه به e.errno می‌توانید مطمئن شوید با «دسترسی رد شده» طرفید یا با حالت خاصی مثل «فایل قفل است» (EAGAIN یا EBUSY) که روی سطح ظاهر شبیه است، اما ریشهٔ متفاوتی دارد.

ریشه‌یابی فنی: چرا مجوز رد می‌شود؟

پرتاب PermissionError همیشه از یک تصمیم هستهٔ سیستمعامل دربارهٔ «آیا این کاربر اجازه دارد این عملیات را روی این منبع انجام دهد؟» می‌آید. این تصمیم از ترکیب چند لایه گرفته می‌شود:

لایهٔ اول: هویت کاربر اجراکننده

مهم‌ترین سؤال این است: «چه کاربری پایتون را اجرا کرده؟». کدی که در ترمینال با کاربر شخصی شما کار می‌کند، ممکن است روی سرور با کاربر www-data، nginx یا apache اجرا شود و به فایل‌های همان کاربر دسترسی نداشته باشد. برای بررسی سریع:

$ whoami
$ id
uid=1001(myuser) gid=1001(myuser) groups=1001(myuser),27(sudo)
$ python3 -c "import os; print(os.getuid(), os.geteuid())"

اگر getuid() و geteuid() متفاوت باشند، یعنی برنامه از طریق setuid اجرا شده و ممکن است مجوزها به شکل غیرقابل‌پیش‌بینی رفتار کنند.

لایهٔ دوم: بیت‌های مجوز فایل

در سیستم‌های POSIX (لینوکس، macOS، BSD) هر فایل سه دستهٔ مجوز دارد: owner، group و others. برای هر دسته سه بیت وجود دارد: خواندن (r)، نوشتن (w) و اجرا (x). این‌ها در قالب نمایش اکتال (مثل 644 یا 755) خلاصه می‌شوند.

یک فایل 600 فقط برای مالک قابل خواندن و نوشتن است. اگر پایتون با کاربر دیگری اجرا شود، با PermissionError مواجه می‌شود. مفهوم کامل مجوزها در مقالهٔ ویکی‌پدیای مجوزهای فایل‌سیستم به‌تفصیل آمده است.

لایهٔ سوم: umask و مجوز پیش‌فرض

هنگام ساخت یک فایل یا پوشه جدید، مجوز نهایی از ترکیب «مجوز درخواستی برنامه» و «ماسک کاربر» (umask) به دست می‌آید. اگر umask روی مقدار سختگیرانه‌ای مثل 077 تنظیم شده باشد، فایل‌های جدید فقط برای مالک قابل دسترسی خواهند بود و کاربران سرویس (مثلاً www-data) نمی‌توانند آن‌ها را بخوانند. همین یک نکته، ریشهٔ درصد قابل‌توجهی از خطاهای تولیدی است.

لایهٔ چهارم: ACL و SELinux

در سرورهای سخت‌گیرتر، علاوه بر بیت‌های سنتی، ACL (Access Control List) و ماژول‌های امنیتی مثل SELinux یا AppArmor هم وارد بازی می‌شوند. در این حالت ممکن است خروجی ls -l نشان دهد همه‌چیز درست است، اما هسته همچنان دسترسی را رد کند. تشخیص این وضعیت با ابزارهایی مثل getfacl، ls -Z و لاگ‌های /var/log/audit/audit.log امکان‌پذیر است.

لایهٔ پنجم: قفل‌شدن فایل توسط فرآیند دیگر

گاهی مجوز واقعاً رد نمی‌شود، بلکه فایل توسط فرآیند دیگری باز و قفل شده است. در ویندوز، این حالت معمولاً با PermissionError ظاهر می‌شود؛ چون ویندوز فایل‌های باز را به‌طور پیش‌فرض انحصاری نگه می‌دارد. در لینوکس، ممکن است با errno.EAGAIN یا EBUSY برخورد کنید که اگر به‌درستی مدیریت نشود، ممکن است به PermissionError ترجمه شود. برای بررسی گلوگاه واقعی، ابزارهایی مثل lsof یا fuser به کار می‌آیند:

$ lsof /path/to/file.log
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
python3  1234 user   3u   REG  8,1     2048  12345 /path/to/file.log

اگر خروجی lsof یک فرآیند فعال را نشان دهد، مشکل «مجوز» نیست؛ «قفل» است. این تفکیک، نیمی از مسیر عیب‌یابی را روشن می‌کند. برای بررسی سیستماتیک ساختار خطاهای پایتون، خطای TypeError در پایتون را هم ببینید تا تفاوت خطاهای منطقی با خطاهای سیستمعامل را بهتر تشخیص دهید.

تفاوت مجوز در POSIX و Windows

پایتون روی هر دو خانوادهٔ سیستمعامل اجرا می‌شود، اما مدل مجوزها در آن‌ها بنیادی متفاوت است. این تفاوت، ریشهٔ بسیاری از کدهای «روی سیستم من کار می‌کند، روی سرور نه» است.

ویژگیPOSIX (Linux / macOS)Windows
مدل مجوزبیت‌های rwx برای owner/group/othersACL مبتنی بر کاربر و گروه
تغییر باchmod و chownicacls و پنل امنیتی فایل
مفهوم اجرابیت x لازم استپسوند و انجمن کافی‌اند
قفل فایلاختیاری و عمدیپیش‌فرض انحصاری در هنگام باز بودن
نتیجهٔ خطاerrno.EACCES (۱۳)errno.EACCES (۵) یا EPERM

در ویندوز، رفتار انحصاری فایل‌ها بسیار رایج است: اگر یک فرآیند دیگر (حتی یک برنامهٔ ویرایشگر متن) فایل را باز نگه دارد، پایتون هنگام نوشتن با PermissionError متوقف می‌شود. همین نکته باعث می‌شود کدی که روی لینوکس سال‌ها بدون مشکل اجرا شده، روی ویندوز توسعه‌دهنده با خطا مواجه شود.

پنج سناریوی رایج در پروژه‌های واقعی

در طول سال‌ها کار با پایتون، خطای PermissionError را تقریباً همیشه در پنج الگوی مشخص دیده‌ام. شناختن این الگوها، تشخیص را چند برابر سریع‌تر می‌کند.

سناریوی اول: نوشتن در مسیرهای سیستمی

رایج‌ترین حالت: اسکریپت سعی می‌کند فایل لاگ یا داده را در /var/log، /etc، /opt یا ریشهٔ پروژهٔ وب‌سرور بنویسد، در حالی که با کاربر عادی اجرا می‌شود. راه‌حل درست، هرگز sudo python3 script.py نیست؛ راه‌حل این است که مقصد نوشتن را به مسیری منتقل کنید که کاربر سرویس به آن دسترسی دارد، یا مجوز پوشه را با chown به همان کاربر بدهید.

سناریوی دوم: خواندن فایل تنظیمات محافظت‌شده

فایل‌هایی مثل .env، settings.py یا کلیدهای SSH معمولاً مجوز 600 دارند. اگر سرویسی با کاربر دیگری اجرا شود، خواندن آن‌ها رد می‌شود. در Django، این وضعیت اغلب به شکل PermissionError: [Errno 13] Permission denied: /path/to/.env ظاهر می‌شود و شکایت‌آمیز است چون فایل «قابل مشاهده» است، ولی «قابل خواندن» نیست.

سناریوی سوم: SQLite و قفل‌شدن دیتابیس

خود SQLite روی فایل کار می‌کند. اگر فایل دیتابیس در پوشه‌ای باشد که کاربر وب‌سرور به آن دسترسی نوشتن ندارد، اولین درج داده‌ها با PermissionError متوقف می‌شود. این خطا مخصوصاً در استقرارهایی که کد را با کاربر root توسعه داده‌اید و بعد سرویس را با www-data اجرا می‌کنید، بسیار رخ می‌دهد.

سناریوی چهارم: پوشه‌های موقت در سرورهای اشتراکی

روی هاست‌های اشتراکی، پوشهٔ /tmp معمولاً روی هر کاربر با umask سختگیرانه جدا شده است. کدی که با tempfile.NamedTemporaryFile() کار می‌کند، اگر بدون توجه به dir مشخص نوشته شود، ممکن است در مسیر اشتباهی قرار گیرد. استفاده از پارامتر dir= در توابع tempfile این مشکل را حل می‌کند.

سناریوی پنجم: اجرای اسکریپت‌های خارجی و ابزارهای خط فرمان

اگر پایتون با subprocess.run() یک باینری خارجی مثل ffmpeg، imagemagick یا یک اسکریپت شل را اجرا کند و بیت اجرا (x) روی آن نباشد، با خطای مجوز مواجه می‌شوید. راه‌حل: chmod +x روی فایل اجرایی، یا استفاده از مفسر صریح مثل bash script.sh به‌جای اجرای مستقیم.

در همین خانواده از پروژه‌ها، اسکریپت‌های وب اسکرپینگ با پایتون هم به‌طور مکرر با این خطا درگیر می‌شوند، چون فایل‌های خروجی CSV و JSON را در مسیرهای دلخواه می‌نویسند.

اگر یک اسکریپت روی سیستم توسعهٔ شما کار می‌کند و روی سرور نه، احتمالاً اولین چیزی که باید بپرسید این است: «این کد با چه کاربری اجرا می‌شود؟» — نه «چه تغییری در منطق بدهم؟».

تشخیص گام‌به‌گام: از Traceback تا ریشه

پیش از هر راه‌حلی، باید با اطمینان بدانید مشکل واقعاً از مجوز است، نه از چیز دیگری. پروتکلی که در پروژه‌های خودم استفاده می‌کنم در پنج گام خلاصه می‌شود:

گام اول: خواندن دقیق Traceback

پیام خطا معمولاً این شکل را دارد:

PermissionError: [Errno 13] Permission denied: '/var/www/app/logs/app.log'

مسیر دقیق فایل داخل پیام آمده است. اگر مسیر یک دایرکتوری است نه فایل، یعنی مشکل روی پوشه است نه فایل. اگر مسیر نسبی است (مثل logs/app.log)، ابتدا مطمئن شوید cwd همان‌جایی است که فکر می‌کنید:

import os
print(os.getcwd())
print(os.path.abspath("logs/app.log"))

گام دوم: بررسی مجوز با ls و stat

در لینوکس و macOS:

$ ls -l /var/www/app/logs/app.log
-rw-r--r-- 1 root root 2048 Sep 15 12:00 app.log
$ stat /var/www/app/logs/app.log

اگر مالک root است و کاربر جاری www-data، به‌طور قطع به نوشتن دسترسی نخواهد داشت — حتی با مجوز 644 که خواندن را برای دیگران باز می‌کند.

گام سوم: بررسی والد و مسیر کامل

یک نکتهٔ ظریف: حتی اگر فایل مجوز نوشتن داشته باشد، اگر پوشهٔ والد مجوز +x نداشته باشد، شما نمی‌توانید حتی آن فایل را باز کنید. این قاعده در لینوکس به‌شکل غیرشهودی عمل می‌کند و در مستندات کلاسیک POSIX توضیح داده شده است.

گام چهارم: تست os.access

پایتون تابعی دارد که می‌توانید پیش از عملیات، اجازه را بررسی کنید:

import os
path = "/var/www/app/logs/app.log"
print("readable:", os.access(path, os.R_OK))
print("writable:", os.access(path, os.W_OK))
print("executable:", os.access(path, os.X_OK))

هشدار مهم: os.access نتیجهٔ احتمالی را برمی‌گرداند، اما به‌دلیل ماهیت TOCTOU (تغییر وضعیت بین بررسی و استفاده) کاملاً قابل اتکا نیست. یعنی ممکن است بگوید «نوشتنی است»، ولی چند میلی‌ثانیه بعد فایل قفل شود. همیشه بعد از os.access، خود عملیات را در بلوک try/except هم اجرا کنید.

گام پنجم: ردیابی در سطح سیستمعامل

اگر هیچ‌کدام از این‌ها مشکل را نشان نداد، به‌سراغ strace (لینوکس) بروید:

$ strace -f -e trace=open,openat,access python3 app.py 2>&1 | grep -i eacces

خروجی EACCES شما را دقیقاً به همان فراخوانی سیستمی می‌رساند که رد شده است. این تکنیک مخصوصاً وقتی کارساز است که پایتون خطا را «بلعیده» و در جای دیگری گزارش کرده باشد.

راه‌حل‌های عملی و امن

پس از تشخیص ریشه، انتخاب راه‌حل باید «کم‌هزینه‌ترین تغییر ساختاری» باشد. هفت راه‌حل زیر را به‌ترتیب اولویت توصیه می‌کنم.

راه‌حل اول: اصلاح مجوز با chmod (سریع اما سطحی)

$ chmod 640 /var/www/app/logs/app.log
$ chmod 755 /var/www/app/logs

640 یعنی مالک می‌تواند بخواند و بنویسد، گروه می‌تواند بخواند، دیگران هیچ‌چیز. این راه‌حل سریع است، اما اگر کاربر اجراکننده عضو گروه مالک نباشد، باز هم کار نمی‌کند.

راه‌حل دوم: تغییر مالکیت با chown (پایدارتر)

$ chown www-data:www-data /var/www/app/logs/app.log

این راه‌حل در سرورهای تولیدی بهترین گزینه است، به شرط آن‌که مطمئن باشید سرویس دقیقاً با همان کاربر اجرا می‌شود. اگر بعداً کاربر را تغییر دهید، مجوزها باید دوباره تنظیم شوند.

راه‌حل سوم: اجرا با sudo — فقط برای دیباگ موقت

گاهی سریع‌ترین راه اثبات مسئله این است که اسکریپت را با sudo python3 script.py اجرا کنید. اگر خطا برطرف شد، فهمیده‌اید ریشه در مجوز کاربر است. اما این راه‌حل هرگز نباید در محیط تولید باقی بماند، چون کل برنامه را با بالاترین سطح دسترسی اجرا می‌کند و یک آسیب‌پذیری کوچک را به یک فاجعه تبدیل می‌کند. پیش از هر آزمایش با sudo، یک بکاپ از وضعیت فعلی بگیرید.

راه‌حل چهارم: مدیریت هوشمندانه در کد

در کد پایتون، بهترین رویکرد «گرفتن استثنا و اجرای بازگشتی امن» است. الگوی زیر را در پروژه‌های خودم بسیار استفاده کرده‌ام:

import logging
from pathlib import Path

def safe_append(path: Path, text: str) -> None:
    try:
        with path.open("a", encoding="utf-8") as f:
            f.write(text)
    except PermissionError as e:
        logging.warning("cannot append to %s: %s", path, e)
        # گزینهٔ پشتیبان: نوشتن در پوشه‌ی خانگی کاربر
        fallback = Path.home() / "app_fallback.log"
        with fallback.open("a", encoding="utf-8") as f:
            f.write(text)

این الگو تضمین می‌کند خطا برنامه را متوقف نکند، ولی همچنان لاگ قابل رهگیری باشد. برای الگوهای بیشتر مدیریت استثنا، مدیریت خطا در پایتون مرجع خوبی است.

راه‌حل پنجم: نوشتن در مسیر مناسب با tempfile

هنگام کار با فایل‌های موقت، همیشه مسیر را صریح مشخص کنید:

import tempfile
from pathlib import Path

with tempfile.NamedTemporaryFile(
    mode="w",
    encoding="utf-8",
    dir=str(Path.home() / "tmp"),
    delete=False,
) as tmp:
    tmp.write("hello")

پوشهٔ tmp را در مسیر خانگی کاربر سرویس بسازید و مجوز 700 برایش بگذارید. این رویکرد، از تعارض با پوشهٔ مشترک /tmp جلوگیری می‌کند.

راه‌حل ششم: خواندن امن فایل‌های حساس

برای فایل‌های تنظیمات، از الگوی زیر استفاده کنید تا خطاها با پیام واضح گزارش شوند:

from pathlib import Path

config = Path("/etc/myapp/settings.env")

if not config.is_file():
    raise SystemExit(f"config file missing: {config}")

try:
    content = config.read_text(encoding="utf-8")
except PermissionError:
    raise SystemExit(
        f"cannot read {config}; check chmod and running user. "
        f"typical fix: chown : {config}"
    )

این کار دیباگ را برای تیم پشتیبانی سرور بسیار آسان‌تر می‌کند، چون پیام خطا خودش راه‌حل را می‌گوید.

راه‌حل هفتم: تغییر ساختار پروژه (پایدارترین راه)

اگر متوجه شدید پروژه شما مدام به مسیرهای سیستم نیاز پیدا می‌کند، احتمالاً ساختار مسیرها اشتباه است. بهترین راه این است که فایل‌های قابل نوشتن را در مسیر زیر قرار دهید:

/var/lib/myapp/       → داده‌های پویا
/var/log/myapp/       → لاگ‌ها
/etc/myapp/           → تنظیمات فقط-خواندنی

و مالکیت همهٔ پوشه‌های نوشتنی را به کاربر سرویس بدهید. این ساختار در توزیع‌های مدرن لینوکس استاندارد است و از به‌روزرسانی‌های بی‌دردسر پشتیبانی می‌کند.

اگر در پروژه‌ای با فایل‌های فراوان سروکار دارید، کار با فایل‌ها در پایتون را مطالعه کنید تا با الگوهای context manager و مدیریت خودکار بستن فایل‌ها آشنا شوید.

PermissionError در Django، Flask و Gunicorn

در محیط وب، خطای مجوز شکل متفاوتی پیدا می‌کند، چون معمولاً پایتون در پس‌زمینه و با کاربری متفاوت از کاربر تعاملی اجرا می‌شود.

Django و فایل settings

در آموزش Django برای مبتدیان دیدیم که فایل settings.py اغلب مقادیر محرمانه را از .env می‌خواند. اگر Gunicorn با کاربر www-data اجرا شود و .env مالک root با مجوز 600 باشد، Django با PermissionError در هنگام import شکست می‌خورد. تشخیص درست:

$ ps -o user= -p $(pgrep -f gunicorn | head -n1)
$ ls -l /srv/app/.env

اگر مالک متفاوتی دیدید، سریع‌ترین اصلاح:

$ chown www-data:www-data /srv/app/.env
$ chmod 640 /srv/app/.env

Flask و پوشهٔ static

در Flask، اگر پوشهٔ static یا uploads توسط کاربر root ساخته شده باشد و اپلیکیشن با کاربر دیگری اجرا شود، آپلود فایل‌های جدید با PermissionError شکست می‌خورد. راه‌حل استاندارد این است که مسیر UPLOAD_FOLDER را صریحاً در تنظیمات تعیین کنید و مالکیت آن را به کاربر سرویس بدهید:

from pathlib import Path

app.config["UPLOAD_FOLDER"] = Path("/var/lib/myapp/uploads")
app.config["UPLOAD_FOLDER"].mkdir(parents=True, exist_ok=True)

سپس در سطح سیستم‌عامل:

$ chown -R www-data:www-data /var/lib/myapp/uploads
$ chmod 750 /var/lib/myapp/uploads

Gunicorn و سوکت Unix

وقتی Gunicorn با سوکت یونیکس کار می‌کند، اگر پوشهٔ سوکت مجوز درست نداشته باشد، Nginx نمی‌تواند به آن متصل شود. این حالت معمولاً با 502 Bad Gateway ظاهر می‌شود، ولی در لاگ Gunicorn ممکن است PermissionError ببینید. پوشهٔ سوکت باید بین دو کاربر (وب‌سرور و اپلیکیشن) به اشتراک گذاشته شود.

نکتهٔ خاص: log rotation

ابزار logrotate گاهی فایل لاگ را می‌چرخاند و فایل جدید با مالکیت root می‌سازد. سرویسی که تا دیروز بدون خطا کار می‌کرد، ناگهان با PermissionError متوقف می‌شود. این وضعیت را در پروژه‌های خودم چندبار دیده‌ام. راه‌حل، تنظیم create در فایل پیکربندی logrotate با کاربر درست است:

/var/log/myapp/*.log {
    daily
    rotate 14
    create 0640 www-data adm
    missingok
    notifempty
}

ملاحظات امنیتی که نباید نادیده بگیرید

راه‌حل‌های سریع برای PermissionError، اگر بی‌دقت اجرا شوند، می‌توانند امنیت سیستم را به خطر بیندازند. چند قاعدهٔ ساده که در پروژه‌های خودم هرگز نقض نمی‌کنم:

یک: chmod 777 تقریباً هرگز پاسخ درست نیست. این مجوز هر کاربری را قادر به نوشتن و اجرای آن فایل می‌کند و در سرورهای عمومی، پنجره‌ای برای آپلود و اجرای کد مخرب باز می‌کند. اگر به chmod 777 رسیده‌اید، یعنی هنوز ریشهٔ مشکل را درست تشخیص نداده‌اید.

دو: برنامه را با root اجرا نکنید، مگر اینکه به دلایل فنی خاص نیاز باشد. روی سرویس‌های مدرن، کاربر اختصاصی با کمترین مجوز ممکن بهترین رویکرد است.

سه: فایل‌های حاوی کلید API، پسورد دیتابیس یا توکن را با مجوز کمتر از 640 رها نکنید. حتی داخل تیم، دسترسی باید محدود باشد.

چهار: لاگ‌ها اطلاعات حساس دارند. مجوز 640 برای لاگ‌های تولیدی توصیه می‌شود، نه 644 که برای همه قابل خواندن است.

پنج: هرگز در کد، مسیری که از ورودی کاربر می‌آید را بدون اعتبارسنجی باز نکنید. حملهٔ path traversal می‌تواند به خواندن فایل‌های خارج از پوشهٔ مجاز منتهی شود. در کنار PermissionError، خطاهایی مثل FileNotFoundError هم ممکن است نشانهٔ همین دسته حملات باشند — مقالهٔ خطای FileNotFoundError در پایتون نکات مکمل را پوشش می‌دهد.

برای مقایسه با سایر خطاهای خانوادهٔ OSError و درک بهتر تفاوت‌ها، خطای OSError در پایتون را نیز ببینید.

هر بار که به فکر chmod 777 می‌افتید، به یاد بیاورید که دارید در را به روی همه باز می‌کنید؛ در حالی که مشکل واقعی احتمالاً یک کاربر اشتباه یا یک مسیر نامناسب است.

پرسش‌های پرتکرار درباره PermissionError

این بخش به پرسش‌هایی می‌پردازد که در جلسات مشاوره و انجمن‌های فنی بیشترین تکرار را داشته‌اند. پاسخ‌ها به‌شکلی طراحی شده‌اند که برای جستجوهای مستقیم و دستیارهای هوش مصنوعی به‌عنوان پاسخ معتبر قابل استخراج باشند.

تفاوت PermissionError و FileNotFoundError چیست؟

FileNotFoundError یعنی فایل در مسیر مشخص‌شده وجود ندارد. PermissionError یعنی فایل وجود دارد اما شما اجازهٔ عملیات روی آن را ندارید. اشتباه رایجی که در تیم‌ها دیده‌ام: توسعه‌دهندهٔ تازه‌کار پیام «دسترسی رد شد» را با «فایل نیست» یکی می‌گیرد، در حالی که این دو راه‌حل‌های کاملاً متفاوتی دارند. برای تفکیک دقیق، در یک اسکریپت آزمایشی این کد را اجرا کنید:

from pathlib import Path

path = Path("/etc/shadow")
print("exists:", path.exists())
print("is_file:", path.is_file())
print("readable:", path.is_file() and os.access(path, os.R_OK))

اگر exists و is_file مقدار True بدهند اما خواندن با خطا مواجه شود، صددرصد مسئله مجوز است.

چرا اجرای اسکریپت روی لینوکس کار می‌کند ولی روی ویندوز خطا می‌دهد؟

سه دلیل اصلی: مدل مجوز متفاوت، قفل انحصاری فایل‌ها در ویندوز، و تفاوت در بیت‌های اجرا. اگر کدی دارید که فایل لاگ را هم‌زمان با خواندن، می‌نویسد، روی ویندوز با خطا مواجه می‌شود، چون ویندوز اجازه نمی‌دهد فایل در دو حالت ناسازگار باز شود. راه‌حل: از mode="a+" با فلگ مناسب استفاده کنید یا فایل را در حافظه بافر کنید و در انتها بنویسید.

آیا استفاده از sudo برای حل دائمی مشکل درست است؟

خیر. sudo فقط برای دیباگ موقت و در محیط غیرتولیدی قابل قبول است. در محیط تولیدی، این کار به یک ریسک امنیتی تبدیل می‌شود: اگر مهاجم بتواند کد را تزریق کند، همه‌چیز با کاربر root اجرا می‌شود و دسترسی به کل سیستم ممکن می‌شود. راه‌حل درست: اصلاح مالکیت با chown و اجرای برنامه با کاربر اختصاصی.

آیا os.access همیشه دقیق است؟

خیر. os.access یک بررسی لحظه‌ای است و به پدیدهٔ TOCTOU (Time-of-check to time-of-use) حساس است. بین زمانی که بررسی می‌کنید و زمانی که عملیات را انجام می‌دهید، ممکن است مجوز فایل تغییر کند یا فایل توسط فرآیند دیگری قفل شود. بهترین رویکرد: os.access را برای گزارش اولیه استفاده کنید، اما همیشه عملیات واقعی را در try/except نگه دارید.

PermissionError در کانتینرهای Docker چرا بیشتر رخ می‌دهد؟

چون در Docker معمولاً چند لایه کار می‌کنیم: کاربر داخل کانتینر، کاربر میزبان فایل‌های mount شده، و مجوز فایل‌های بین میزبان و کانتینر. اگر پوشه‌ای از میزبان با -v داخل کانتینر mount شود، مجوزهای میزبان اعمال می‌شوند نه مجوزهای داخل کانتینر. اگر UID میزبان و UID داخل کانتینر یکی نباشد، عملیات نوشتن رد می‌شود. راه‌حل: در فایل Dockerfile یک کاربر با UID مشخص بسازید و با --user همان UID را در زمان اجرا مقید کنید:

RUN useradd -u 1000 -m appuser
USER appuser

و در زمان اجرا:

$ docker run --user 1000:1000 myapp

این تنها رویکردی است که در محیط‌های containerized به‌طور پایدار کار می‌کند.

PermissionError هنگام import یک ماژول به چه معناست؟

گاهی خطای مجوز نه از کد خود شما، بلکه از یک ماژول نصب‌شده می‌آید. اگر پکیجی در site-packages با کاربر root نصب شده باشد و بعد کاربر عادی بخواهد محتوایش را بخواند، این اتفاق می‌افتد. راه‌حل: از pip install --user یا محیط مجازی (venv) استفاده کنید. الگوی مشابه در مقالهٔ رفع ModuleNotFoundError در پایتون هم بررسی شده است.

آیا تغییر مجوز روی فایل، روی بکاپ و لاگ‌روتیشن تأثیر دارد؟

بله. اگر مجوز یک فایل لاگ را تغییر دهید، ابزارهایی مثل logrotate ممکن است دچار خطا شوند، چون سیاست خودشان را دارند. پس از هر تغییر مجوز، حتماً یک چرخهٔ logrotate --force اجرا کنید تا مطمئن شوید همه‌چیز سر جای خودش است.

آیا می‌توان به‌طور پیش‌فرض مجوز فایل‌های جدید در پایتون را تغییر داد؟

بله. با استفاده از os.umask می‌توانید ماسک کاربر را موقتاً تغییر دهید:

import os
from pathlib import Path

old = os.umask(0o027)
try:
    Path("/tmp/secure.log").write_text("hello")
finally:
    os.umask(old)

ماسک 027 باعث می‌شود فایل‌های جدید با مجوز 640 ساخته شوند. این الگو را در سرورهای حساس زیاد استفاده کرده‌ام.

اگر خطا فقط روی بعضی فایل‌ها رخ می‌دهد، از کجا شروع کنم؟

ابتدا با find فایل‌هایی که مجوز متفاوتی دارند را پیدا کنید:

$ find /var/www/app -type f ! -perm 640 -ls

سپس با یک اسکریپت پایتونی، فایل‌های مشکل‌دار را فهرست کنید:

import os
from pathlib import Path

for p in Path("/var/www/app").rglob("*"):
    if p.is_file() and not os.access(p, os.W_OK):
        print(p)

این کار، نقشهٔ کاملی از فایل‌هایی که با کاربر جاری ناسازگارند به شما می‌دهد.

درس‌هایی که از دیباگ مجوز فایل گرفتم

خطای PermissionError بیش از آنکه یک باگ باشد، یک آینه است: نشان می‌دهد معماری استقرار شما در چه نقطه‌ای با واقعیت سیستمعامل ناسازگار است. سه اصلی که در طول سال‌ها به آن رسیده‌ام:

اصل اول: مسئولیت را روشن کنید. هر پوشه باید مالک مشخصی داشته باشد و همان کاربر مسئول خواندن و نوشتن در آن باشد. وقتی چند کاربر روی یک پوشه می‌نویسند، باید گروه مشترک داشته باشند، نه اینکه همه به 777 پناه ببرند.

اصل دوم: پیام خطا را دوست خود بدانید. پایتون دقیقاً به شما می‌گوید کدام فایل و چه عملیاتی رد شده است. اگر با except عمومی و بی‌تفکر این پیام را بلعید، بعداً دیباگ چند برابر سخت‌تر می‌شود. همیشه حداقل errno و مسیر را لاگ کنید.

اصل سوم: ساختار پروژه را با سیستمعامل هم‌آهنگ کنید. اگر پروژه‌تان با ساختار استاندارد /etc، /var/log و /var/lib جلو برود، بسیاری از این خطاها از ابتدا شکل نمی‌گیرند. این رویکرد، در دنیای Linux و کانتینرها، هزینهٔ نگهداری را به‌مراتب پایین‌تر می‌آورد.

در پایان، اگر روی سرور شما هم مکرر این خطا رخ می‌دهد، توصیه می‌کنم ابتدا وضعیت کاربر سرویس را با ps aux | grep python بررسی کنید و سپس با یک اسکریپت کوتاه، فایل‌های ناسازگار را فهرست کنید. تجربهٔ من این است که ۹۰٪ از پرونده‌های PermissionError در پروژه‌های واقعی، در همین دو گام قابل حل‌اند. اگر در پروژهٔ خودتان با حالت خاصی برخورد کردید که اینجا پوشش داده نشده — مثلاً در ترکیب Celery با Systemd یا در فایل‌سیستم‌های شبکه‌ای NFS — تجربه‌تان را در دیدگاه‌ها بنویسید؛ به‌ویژه اگر راه‌حل متفاوتی پیدا کرده‌اید که می‌تواند برای خوانندهٔ بعدی مفید باشد. 🔧