Skip to main content

บอร์ดประกาศ — บันไดสิทธิ์และการตัดสินว่าใครทำอะไรได้

ภาพรวม

ชั้นสิทธิ์ของโดเมน bulletin ทั้งหมด ทุก request ไม่ว่าจะเป็นการอ่านหรือการเขียนจะวิ่งผ่าน resolveAccess เพียงครั้งเดียวต่อ request จากนั้น AccessContext ที่ได้จะถูกส่งต่อลงไปให้ทุกชั้น โดย ไม่มี handler ตัวใดคำนวณสิทธิ์ใหม่

ควรอ่านหัวข้อนี้ก่อนฟีเจอร์ bulletin อื่นทั้งหมด เพราะกฎ 3 ชุดในนี้ ได้แก่ สิทธิ์ตาม user_type, การ block และการมองเห็นหมวดหมู่ เป็นตัวกำหนดพฤติกรรมของฟีด การโพสต์ การคอมเมนต์ และรีแอ็กชันทั้งหมด

Business Flow

resolveAccess(c) — 5 ขั้นตอนที่ลำดับมีความสำคัญมาก

เหตุผลของลำดับนี้คือ LIFF token ของผู้เรียกมีความหมายก็ต่อเมื่อผูกกับ LINE Login channel ของ OA นั้นแล้วเท่านั้น ทุกอย่างที่มี side effect เช่น การสร้างแถว board หรือการ provision guest จึงต้องเกิดขึ้น หลัง การ bind token ผลคือผู้เรียกที่ยังไม่ได้ยืนยันตัวตนหรือมาจาก channel อื่นจะเขียนแถวใด ๆ ไม่ได้เลย

  1. Resolve OA และ LINE Login channel ของ OA นั้นจาก :hash — ไม่พบตอบ 404 board not found
  2. Verify token กับ channel นั้นโดยเฉพาะ ผ่าน resolveUserID — ไม่ผ่านตอบ 401
  3. EnsureBoard — สร้างแถว bulletin.board ให้ OA หากยังไม่มี
  4. ค้นหาแถว line_user โดยการค้นไม่พบ ไม่ใช่ข้อพิสูจน์ว่าไม่มีแถวอยู่จริง เพราะ query กรอง status และ deleted_date อยู่ ระบบจึงเรียก EnsureGuest ซึ่ง insert แบบ conflict-safe และรายงานกลับว่าในตารางจริงมีอะไรอยู่ — แถวที่มีอยู่แต่ถูกปิดหรือถูกลบถือเป็น การปฏิเสธโดยเจตนา ตอบ 403 BULLETIN_USER_INACTIVE ไม่ใช่การรับเข้าใหม่เป็น guest
  5. โหลด block ที่ยัง active แล้วประกอบ AccessContext ซึ่งบรรจุ lineUserID, displayName, userType, lineOaID, oaName, oaCover, orgID, boardID, settings, audienceIDs และ block

Settings และการตัดสินสิทธิ์

คอลัมน์ bulletin.board.settings เป็น jsonb โดย ParseSettings จะ unmarshal ทับลงบนค่า default ดังนั้น key ที่ blob ไม่มีจะคงค่า default ไว้ และ JSON ที่พังรูปจะ degrade เป็น default แทนที่จะกลายเป็นบอร์ดที่ปฏิเสธทุกคน

ค่า view_access และ write_access ที่เป็น array ว่างถือเป็น deny-all ที่ถูกต้องตามเจตนา ส่วนค่า null หมายถึง "ไม่ได้ตั้ง" จึงกลับไปใช้ค่า default

ค่า default ของ settings ได้แก่ view_access เป็น member และ guest, write_access เป็น member เท่านั้น, require_post_approval และ require_comment_approval เป็น false, max_images_per_post เท่ากับ 4, max_images_per_comment เท่ากับ 2, max_body_length เท่ากับ 2000, max_comment_length เท่ากับ 500, allow_comments_default เป็น true, allow_user_reports เป็น true และ reaction_emojis เป็นชุด emoji มาตรฐาน 6 ตัว

Capability ทั้ง 5 ตัว

เมธอดกฎ
CanView()user_type ต้องอยู่ใน view_access มิฉะนั้นตอบ 403 BULLETIN_VIEW_FORBIDDEN
CanWrite()ต้องอยู่ใน write_access มิฉะนั้นตอบ BULLETIN_WRITE_FORBIDDEN และ ต้องไม่มี block ที่ scope ไม่ใช่ comment โดยใช้หลัก deny-by-default คือ scope ที่พิมพ์ผิดหรือเป็น zero value ถือเป็น block เต็มรูปแบบ ตอบ BULLETIN_BLOCKED
CanComment()ต้องอยู่ใน write_access และ ต้องไม่มี block ใด ๆ เลย เพราะ block ทั้งสอง scope ห้ามคอมเมนต์
CanReact()เท่ากับ CanWrite() ดังนั้นคนที่ถูก mute ด้วย scope comment ยังกดรีแอ็กชันได้
CanReport()ต้องเปิด allow_user_reports มิฉะนั้นตอบ BULLETIN_REPORTS_DISABLED แล้วจึงตรวจ CanWrite() ต่อ

Block ที่มี expires_date เป็นเวลาในอดีตถือว่าไม่มีผล ซึ่ง repository กรองให้อยู่แล้วและการตรวจในชั้นนี้เป็นเข็มขัดนิรภัยชั้นที่สอง

การมองเห็นหมวดหมู่ (CanSeeCategory) — 2 แกนที่เป็นอิสระต่อกัน

  • แกนอ่าน กำหนดด้วย access_mode — ค่า inherit ใช้สิทธิ์ระดับบอร์ดซึ่ง guest เห็นด้วย; ค่า restricted ที่ audience ว่างหมายถึง member ทุกคน; ค่า restricted ที่มี audience หมายถึงเฉพาะ member ที่ audience ทับกัน โดย restricted บังคับ member-only เสมอ เพราะ guest ไม่เคยอยู่ใน audience ที่มีความหมาย
  • แกนเขียน กำหนดด้วย post_access — ค่า member ซึ่งเป็น default ให้ LINE user โพสต์ได้ ส่วนค่า admin อนุญาตให้โพสต์จาก CMS เท่านั้น
  • ทั้งสองแกนประกอบกันได้ เช่น inherit คู่กับ admin จะได้หมวด "ประกาศ" ที่ทุกคนอ่านได้แต่มีเพียงแอดมินที่โพสต์ได้
  • ค่า audience_ids ที่อ่านเป็น array ไม่ได้ เช่น literal null หรือ array ที่ทุก element ไม่ใช่จำนวนเต็ม จะถือเป็น deny ไม่ใช่การขยายเป็น member ทุกคน มีเพียงค่าที่ไม่มีเลย (ความยาว 0) เท่านั้นที่ถือเป็น array ว่างซึ่งหมายถึง member ทั้งหมด
  • กฎชุดเดียวกันนี้มี ฝาแฝดที่เขียนเป็น SQL ชื่อ categoryVisibilitySQL ซึ่งใช้กรองฟีด ทั้งสองด้านต้องให้ผลตรงกันและมี test ตรึงคู่กันไว้
  • GET /bulletin/:hash กรองรายการหมวดหมู่ด้วยกฎนี้ตอนส่งออก เพราะ ชื่อหมวดหมู่เองอาจเป็นความลับ จึงต้องถูกตัดออกทั้งก้อน

ชุด error code

ระบบส่ง error code เป็น message เพื่อให้ client แยก branch ได้ ประกอบด้วย BULLETIN_VIEW_FORBIDDEN, BULLETIN_WRITE_FORBIDDEN, BULLETIN_BLOCKED, BULLETIN_REPORTS_DISABLED, BULLETIN_COMMENTS_CLOSED, BULLETIN_IMAGE_LIMIT, BULLETIN_BODY_TOO_LONG, BULLETIN_EMPTY_BODY, BULLETIN_EMPTY_TITLE, BULLETIN_TITLE_TOO_LONG, BULLETIN_INVALID_EMOJI, BULLETIN_INVALID_REASON, BULLETIN_DETAIL_TOO_LONG, BULLETIN_INVALID_IMAGE, BULLETIN_CATEGORY_ADMIN_ONLY และ BULLETIN_USER_INACTIVE

กฎประจำ package ที่ควรจำ

id ของ bulletin เป็น global serial ดังนั้น ทุก query ต้องรับ lineOaID เข้าไปด้วย เพราะ id เพียงตัวเดียวไม่เคยพิสูจน์ความเป็นเจ้าของ และความล้มเหลวทุกรูปแบบ ไม่ว่าจะเป็นไม่มีอยู่ อยู่คนละ tenant ถูกลบ หรืออยู่ในหมวดที่มองไม่เห็น ต้องยุบเป็น 404 เดียวกันทั้งหมด ไม่ใช่ 403 ซึ่งจะเป็นการยืนยันว่าของนั้นมีอยู่จริง

ไฟล์และฟังก์ชันหลัก

ไฟล์ฟังก์ชัน
internal/bulletin/handler.go(*Handler).resolveAccess, interface boardRepo
internal/bulletin/accessctx.goAccessContext, CanView, CanWrite, CanComment, CanReact, CanReport, CanSeeCategory, activeBlock, decodeAudienceIDs, resolveUserID, ChannelBinding, channelBindingOf และชุด error code
internal/bulletin/entity.goSettings, DefaultSettings, ParseSettings, Allows, AllowsEmoji, Category, AllowsLineUserPosts และค่าคงที่ของ status, author, target, scope, access-mode, post-access
internal/bulletin/repository.goFindOaByHash, EnsureBoard, FindLineUser, EnsureGuest, FindActiveBlock, FindCategories, categoryVisibilitySQL
internal/bulletin/view.govisibleCategoryViews, NewCategoryView, publicURLOf
internal/bulletin/register.goMount /bulletin/:hash พร้อม appguard.AppEnabledGuard(db,"bulletin")

จุดเชื่อมต่อกับ Service อื่น

  • ฐานข้อมูล — ตาราง bulletin.board (jsonb settings), bulletin.category, bulletin.block, line_oa และ line_user
  • App enabled guard — เปิดหรือปิดทั้งกลุ่มฟีเจอร์นี้ได้จาก platform admin
  • LIFF authentication — ใช้บันไดยืนยันตัวตนแบบที่ผูก channel
  • ฟีเจอร์ที่เกี่ยวข้อง — เป็นฐานของฟีเจอร์บอร์ดและฟีด, การเขียนโพสต์, การคอมเมนต์ และรีแอ็กชัน/การรายงาน
  • client-web — ตรงกับฟีเจอร์ bulletin-moderation ซึ่งระบบ capability ฝั่งเว็บอ่านจาก settings ที่ส่งมา และเป็นฐานของฟีเจอร์ bulletin-board