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