کنترل دسترسی به فیلدهای خاص

این صفحه بر اساس مفاهیم موجود در «ساختاردهی قوانین امنیتی» و «شرایط نوشتن برای قوانین امنیتی» بنا شده است تا توضیح دهد که چگونه می‌توانید Cloud Firestore Security Rules برای ایجاد قوانینی استفاده کنید که به مشتریان اجازه می‌دهد عملیات را روی برخی از فیلدهای یک سند انجام دهند اما روی بقیه نه.

ممکن است مواقعی پیش بیاید که بخواهید تغییرات یک سند را نه در سطح سند، بلکه در سطح فیلد کنترل کنید.

برای مثال، ممکن است بخواهید به یک کلاینت اجازه دهید یک سند را ایجاد یا تغییر دهد، اما به آنها اجازه ویرایش فیلدهای خاصی در آن سند را ندهید. یا ممکن است بخواهید اعمال کنید که هر سندی که یک کلاینت همیشه ایجاد می‌کند، شامل مجموعه‌ای از فیلدهای خاص باشد. این راهنما نحوه انجام برخی از این وظایف را با استفاده از Cloud Firestore Security Rules پوشش می‌دهد.

اجازه دسترسی خواندن فقط برای فیلدهای خاص

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

اگر فیلدهای خاصی در یک سند وجود دارد که می‌خواهید از برخی کاربران پنهان نگه دارید، بهترین راه این است که آنها را در یک سند جداگانه قرار دهید. برای مثال، می‌توانید ایجاد یک سند در یک زیرمجموعه private مانند زیر را در نظر بگیرید:

/کارمندان/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/کارمندان/{emp_id}/خصوصی/امور مالی

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

سپس می‌توانید قوانین امنیتی اضافه کنید که سطوح دسترسی متفاوتی برای دو مجموعه داشته باشند. در این مثال، ما از custom auth claims استفاده می‌کنیم تا بگوییم فقط کاربرانی که role custom auth claim آنها برابر با Finance است، می‌توانند اطلاعات مالی یک کارمند را مشاهده کنند.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

محدود کردن فیلدها در ایجاد سند

Cloud Firestore بدون طرح است، به این معنی که هیچ محدودیتی در سطح پایگاه داده برای فیلدهای موجود در یک سند وجود ندارد. اگرچه این انعطاف‌پذیری می‌تواند توسعه را آسان‌تر کند، مواقعی وجود خواهد داشت که می‌خواهید مطمئن شوید که مشتریان فقط می‌توانند اسنادی را ایجاد کنند که حاوی فیلدهای خاصی هستند یا فیلدهای دیگری را شامل نمی‌شوند.

شما می‌توانید این قوانین را با بررسی متد keys از شیء request.resource.data ایجاد کنید. این لیستی از تمام فیلدهایی است که کلاینت سعی در نوشتن آنها در این سند جدید دارد. با ترکیب این مجموعه از فیلدها با توابعی مانند hasOnly() یا hasAny() ، می‌توانید منطقی اضافه کنید که انواع اسنادی را که کاربر می‌تواند به Cloud Firestore اضافه کند، محدود می‌کند.

الزام فیلدهای خاص در اسناد جدید

فرض کنید می‌خواهید مطمئن شوید که تمام اسناد ایجاد شده در یک مجموعه restaurant حداقل شامل فیلدهای name ، location و city هستند. می‌توانید این کار را با فراخوانی hasAll() در لیست کلیدها در سند جدید انجام دهید.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

این امر امکان ایجاد رستوران‌ها با فیلدهای دیگر را نیز فراهم می‌کند، اما تضمین می‌کند که تمام اسناد ایجاد شده توسط یک مشتری حداقل شامل این سه فیلد باشند.

ممنوع کردن فیلدهای خاص در اسناد جدید

به طور مشابه، می‌توانید با استفاده از hasAny() در مقابل لیستی از فیلدهای ممنوعه، از ایجاد اسنادی که حاوی فیلدهای خاصی هستند توسط کلاینت‌ها جلوگیری کنید. اگر سندی حاوی هر یک از این فیلدها باشد، این متد مقدار true را ارزیابی می‌کند، بنابراین احتمالاً می‌خواهید نتیجه را منفی کنید تا فیلدهای خاصی را ممنوع کنید.

برای مثال، در مثال زیر، کلاینت‌ها مجاز به ایجاد سندی نیستند که حاوی فیلد average_score یا rating_count باشد، زیرا این فیلدها توسط فراخوانی سرور در مرحله‌ی بعدی اضافه خواهند شد.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

ایجاد لیست مجاز فیلدها برای اسناد جدید

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

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

ترکیب فیلدهای اجباری و اختیاری

شما می‌توانید عملیات hasAll و hasOnly را در قوانین امنیتی خود با هم ترکیب کنید تا برخی از فیلدها را الزامی و برخی دیگر را مجاز کنید. برای مثال، این مثال الزام می‌کند که تمام اسناد جدید حاوی فیلدهای name ، location و city باشند و به صورت اختیاری فیلدهای address ، hours و cuisine را مجاز می‌داند.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

در یک سناریوی دنیای واقعی، ممکن است بخواهید این منطق را به یک تابع کمکی منتقل کنید تا از تکرار کد خود جلوگیری کنید و فیلدهای اختیاری و ضروری را راحت‌تر در یک لیست واحد ترکیب کنید، مانند این:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

محدود کردن فیلدها در هنگام به‌روزرسانی

یک رویه امنیتی رایج این است که فقط به کلاینت‌ها اجازه ویرایش برخی از فیلدها و نه بقیه را بدهید. شما نمی‌توانید این کار را صرفاً با نگاه کردن به لیست request.resource.data.keys() که در بخش قبلی توضیح داده شد، انجام دهید، زیرا این لیست نشان دهنده کل سند است، همانطور که پس از به‌روزرسانی از آن مراقبت خواهد کرد و بنابراین شامل فیلدهایی می‌شود که کلاینت تغییر نداده است.

با این حال، اگر از تابع diff() استفاده کنید، می‌توانید request.resource.data با شیء resource.data مقایسه کنید، که نشان دهنده سند موجود در پایگاه داده قبل از به‌روزرسانی است. این یک شیء mapDiff ایجاد می‌کند، که شیء‌ای است که شامل تمام تغییرات بین دو نقشه مختلف است.

با فراخوانی متد affectedKeys() در این mapDiff، می‌توانید مجموعه‌ای از فیلدها را که در یک ویرایش تغییر کرده‌اند، ایجاد کنید. سپس می‌توانید از توابعی مانند hasOnly() یا hasAny() استفاده کنید تا مطمئن شوید که این مجموعه شامل موارد خاصی می‌شود (یا نمی‌شود).

جلوگیری از تغییر برخی فیلدها

با استفاده از متد hasAny() روی مجموعه تولید شده توسط affectedKeys() و سپس نفی نتیجه، می‌توانید هر درخواست کلاینتی را که سعی در تغییر فیلدهایی دارد که نمی‌خواهید تغییر کنند، رد کنید.

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

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

اجازه تغییر فقط فیلدهای خاص

به جای مشخص کردن فیلدهایی که نمی‌خواهید تغییر کنند، می‌توانید از تابع hasOnly() برای مشخص کردن لیستی از فیلدهایی که می‌خواهید تغییر کنند نیز استفاده کنید. این روش عموماً امن‌تر در نظر گرفته می‌شود زیرا نوشتن در هر فیلد سند جدید به طور پیش‌فرض مجاز نیست تا زمانی که صریحاً آنها را در قوانین امنیتی خود مجاز کنید.

برای مثال، به جای اینکه فیلدهای average_score و rating_count را غیرفعال کنید، می‌توانید قوانین امنیتی ایجاد کنید که به کلاینت‌ها اجازه دهد فقط فیلدهای name ، location ، city ، address ، hours و cuisine را تغییر دهند.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

این یعنی اگر در نسخه‌های بعدی برنامه‌تان، اسناد رستوران شامل فیلد telephone باشند، تلاش برای ویرایش آن فیلد تا زمانی که به عقب برنگردید و آن فیلد را به لیست hasOnly() در قوانین امنیتی خود اضافه نکنید، با شکست مواجه خواهد شد.

اعمال انواع فیلد

یکی دیگر از اثرات بدون طرح بودن Cloud Firestore این است که هیچ اجباری در سطح پایگاه داده برای اینکه چه نوع داده‌هایی می‌توانند در فیلدهای خاص ذخیره شوند، وجود ندارد. با این حال، این چیزی است که می‌توانید در قوانین امنیتی با عملگر is اعمال کنید.

برای مثال، قانون امنیتی زیر الزام می‌کند که فیلد score یک نقد باید یک عدد صحیح، فیلدهای headline ، content و author_name رشته و review_date یک مهر زمانی باشد.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

انواع داده‌های معتبر برای عملگر is عبارتند از bool ، bytes ، float ، int ، list ، latlng ، number ، path ، map ، string و timestamp . عملگر is همچنین از انواع داده‌های constraint ، duration ، set و map_diff پشتیبانی می‌کند، اما از آنجایی که این نوع داده‌ها توسط خود زبان قوانین امنیتی تولید می‌شوند و توسط کلاینت‌ها تولید نمی‌شوند، به ندرت در اکثر برنامه‌های کاربردی از آنها استفاده می‌شود.

انواع داده‌های list و map از ژنریک‌ها یا آرگومان‌های نوع پشتیبانی نمی‌کنند. به عبارت دیگر، می‌توانید از قوانین امنیتی برای اعمال این که یک فیلد خاص شامل یک لیست یا یک نقشه باشد استفاده کنید، اما نمی‌توانید اعمال کنید که یک فیلد شامل لیستی از تمام اعداد صحیح یا تمام رشته‌ها باشد.

به طور مشابه، می‌توانید از قوانین امنیتی برای اعمال مقادیر نوع برای ورودی‌های خاص در یک لیست یا یک نقشه (به ترتیب با استفاده از نمادگذاری براکت یا نام کلیدها) استفاده کنید، اما هیچ راه میانبری برای اعمال انواع داده همه اعضای یک نقشه یا یک لیست به طور همزمان وجود ندارد.

برای مثال، قوانین زیر تضمین می‌کنند که فیلد tags در یک سند حاوی یک لیست باشد و اولین ورودی آن یک رشته باشد. همچنین تضمین می‌کند که فیلد product حاوی یک نقشه باشد که به نوبه خود شامل نام محصول به صورت رشته و مقدار آن به صورت عدد صحیح است.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

نوع فیلدها باید هم هنگام ایجاد و هم هنگام به‌روزرسانی یک سند اعمال شوند. بنابراین، ممکن است بخواهید ایجاد یک تابع کمکی را در نظر بگیرید که بتوانید آن را در هر دو بخش ایجاد و به‌روزرسانی قوانین امنیتی خود فراخوانی کنید.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

اعمال انواع برای فیلدهای اختیاری

مهم است به یاد داشته باشید که فراخوانی request.resource.data.foo در سندی که foo در آن وجود ندارد، منجر به خطا می‌شود و بنابراین هر قانون امنیتی که این فراخوانی را انجام دهد، درخواست را رد می‌کند. می‌توانید این وضعیت را با استفاده از متد get در request.resource.data مدیریت کنید. متد get به شما امکان می‌دهد در صورت وجود نداشتن فیلد، یک آرگومان پیش‌فرض برای فیلدی که از نقشه بازیابی می‌کنید، ارائه دهید.

برای مثال، اگر اسناد بررسی شامل یک فیلد اختیاری photo_url و یک فیلد اختیاری tags نیز باشند که می‌خواهید تأیید کنید به ترتیب رشته و لیست هستند، می‌توانید این کار را با بازنویسی تابع reviewFieldsAreValidTypes به چیزی شبیه به زیر انجام دهید:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

این روش، اسنادی را که در آن‌ها tags وجود دارد، اما لیست نیست، رد می‌کند، در حالی که همچنان اسنادی را که حاوی فیلد tags (یا photo_url ) نیستند، مجاز می‌داند.

نوشتن‌های جزئی هرگز مجاز نیستند

نکته آخر در مورد Cloud Firestore Security Rules این است که آنها یا به مشتری اجازه می‌دهند تغییری در یک سند ایجاد کند، یا کل ویرایش را رد می‌کنند. شما نمی‌توانید قوانین امنیتی ایجاد کنید که نوشتن در برخی از فیلدهای سند شما را بپذیرد در حالی که سایر فیلدها را در همان عملیات رد کند.