สถาปัตยกรรม
สถาปัตยกรรมIFTTT ผ่าน Directives

IFTTT ผ่าน Directives

Gato GraphQL มีความสามารถในการนำกลยุทธ์ IFTTT (If This Then That) มาใช้งานผ่าน directives โดย directives เหล่านี้จะถูกเพิ่มเข้าสู่ query โดยอัตโนมัติเมื่อพบ field หรือ directive ที่ระบุไว้ใน query

โดยทั่วไป IFTTT คือกฎที่กระตุ้นให้เกิดการกระทำเมื่อเหตุการณ์ที่กำหนดไว้เกิดขึ้น ในกรณีของเรา คู่ของเหตุการณ์/การกระทำมีดังนี้

  • ถ้า "พบ field X ใน query" แล้ว "แนบ directive Y เข้ากับ field X"
  • ถ้า "พบ directive Z ใน query" แล้ว "รัน directive Y ก่อน/หลัง directive Z"

การเพิ่ม IFTTT directives เข้าสู่ schema โดยอัตโนมัตินั้นเป็นกระบวนการแบบวนซ้ำ (recursive): directive นั้นๆ สามารถกำหนดชุด IFTTT directives ของตัวเองได้ ซึ่งจะถูกเพิ่มเข้าไปใน directive chain ด้วยเช่นกัน

ใช้งานที่ไหน

ภายใต้การทำงาน clients ใน Gato GraphQL ใช้กลไกนี้ในการกำหนดค่า GraphQL schema

ตัวอย่างเช่น Access Control ช่วยให้เราเลือกกฎการควบคุมการเข้าถึงที่จะนำไปใช้กับ operations, fields และ directives การใช้ IFTTT ทำให้กฎเหล่านี้ถูกนำไปใช้กับองค์ประกอบเหล่านั้นใน GraphQL schema

Access control entry

โดยทั่วไป ต่อไปนี้คือกรณีการใช้งานบางส่วน

กำหนด cache control max-age เป็นรายฟิลด์

แนบ directive @CacheControl เข้ากับทุก fields โดยปรับแต่งค่าของพารามิเตอร์ maxAge: 1 ปีสำหรับ field url ของ Post และ 1 ชั่วโมงสำหรับ field title

ตั้งค่า access control

แนบ directive @validateDoesLoggedInUserHaveAnyRole เข้ากับ field email ของ type User เพื่อให้เฉพาะผู้ดูแลระบบเท่านั้นที่สามารถ query email ของผู้ใช้ได้

ซิงโครไนซ์ access-control กับ cache-control

ด้วยการเชื่อมต่อ directives เป็นลูกโซ่ เราสามารถมั่นใจได้ว่าเมื่อมีการตรวจสอบว่าผู้ใช้สามารถเข้าถึง field/directive ได้ response จะไม่ถูก cache ตัวอย่างเช่น

  • แนบ directive @validateIsUserLoggedIn เข้ากับ field me
  • แนบ directive @CacheControl ที่มีค่า argument maxAge เป็น 0 เข้ากับ directive @validateIsUserLoggedIn

เสริมความปลอดภัย

แนบ directive @validateIsUserLoggedIn เข้ากับ directive @translate เพื่อป้องกันผู้ไม่หวังดีจากการรัน queries กับ GraphQL service ที่อาจทำให้เซิร์ฟเวอร์ล่มและค่าใช้จ่ายพุ่งสูงขึ้น (ในกรณีนี้ @translate ใช้ Google Translate และต้องจ่ายค่าธรรมเนียมในการใช้บริการนี้)

วิธีการทำงาน

เราเพิ่ม directives เข้าสู่ schema ผ่าน IFTTT ได้อย่างไร? สมมติว่าเราต้องการสร้าง custom directive @authorize(role: String!) เพื่อตรวจสอบว่าผู้ใช้ที่รัน field myPosts มี role ที่คาดหวังคือ author หรือไม่ ถ้าไม่ใช่ให้แสดงข้อผิดพลาด

ถ้าเราสร้าง schema โดยใช้ SDL จะมีลักษณะดังนี้

directive @authorize(role: String!) on FIELD_DEFINITION
 
type User {
  myPosts: [Post] @authorize(role: "author")
}

กฎ IFTTT กำหนดความหมายเดียวกับที่ SDL ข้างต้นประกาศไว้: เมื่อใดก็ตามที่มีการร้องขอ field myPosts ให้รัน directive @authorize(role: "author") กับ field นั้น จากนั้นเมื่อพบ field myPosts ใน query engine จะแนบ @authorize(role: 'author') เข้ากับ field นั้นใน executable query โดยอัตโนมัติ

กฎ IFTTT ยังสามารถถูกกระตุ้นเมื่อพบ directive ไม่ใช่แค่ field เท่านั้น ตัวอย่างเช่น เราสามารถตั้งกฎว่า "เมื่อใดก็ตามที่พบ directive @translate ใน query ให้รัน directive @cache(time: 3600) กับ field นั้น"

การเพิ่ม IFTTT directives เข้าสู่ query เป็นกระบวนการแบบวนซ้ำ: มันจะกระตุ้นให้เกิดเหตุการณ์ใหม่ที่จะถูกประมวลผลโดยกฎ IFTTT ซึ่งอาจนำไปสู่การแนบ directives อื่นๆ เข้าสู่ query และดำเนินต่อไปเรื่อยๆ

ตัวอย่างเช่น กฎว่า "เมื่อพบ directive @cache ให้รัน directive @log" จะบันทึก entry เกี่ยวกับการรัน field และกระตุ้นให้เกิดเหตุการณ์ใหม่เกี่ยวกับ directive ที่เพิ่มเข้ามาใหม่นี้

การตั้งค่าผ่านโค้ด PHP

Type User มี fields roles และ capabilities ซึ่งอาจถือเป็นข้อมูลที่ละเอียดอ่อน ดังนั้นจึงไม่ควรให้ผู้ใช้ทั่วไปเข้าถึงได้

ดังนั้นเราสามารถแนบ directive @validateDoesLoggedInUserHaveAnyRole เข้ากับสอง fields นี้ โดยกำหนดค่าให้ตรวจสอบว่าเฉพาะผู้ใช้ที่มี role ที่กำหนด (กำหนดผ่าน environment variable) เท่านั้นที่สามารถเข้าถึงได้ การกำหนดค่าถูกระบุผ่าน CompilerPass

$accessControlManagerDefinition = $containerBuilderWrapper->getDefinition(AccessControlManagerInterface::class);
 
if ($roles = Environment::anyRoleLoggedInUserMustHaveToAccessRolesFields()) {
  $accessControlManagerDefinition->addMethodCall(
    'addEntriesForFields',
    [
      UserRolesAccessControlGroups::ROLES,
      [
        [RootObjectTypeResolver::class, 'roles', $roles],
        [UserObjectTypeResolver::class, 'roles', $roles],
        [RootObjectTypeResolver::class, 'capabilities', $roles],
        [UserObjectTypeResolver::class, 'capabilities', $roles],
      ]
    ]
  );
}
if ($capabilities = Environment::anyCapabilityLoggedInUserMustHaveToAccessRolesFields()) {
  $accessControlManagerDefinition->addMethodCall(
    'addEntriesForFields',
    [
      UserCapabilitiesAccessControlGroups::CAPABILITIES,
      [
        [RootObjectTypeResolver::class, 'roles', $capabilities],
        [UserObjectTypeResolver::class, 'roles', $capabilities],
        [RootObjectTypeResolver::class, 'capabilities', $capabilities],
        [UserObjectTypeResolver::class, 'capabilities', $capabilities],
      ]
    ]
  );
}

เมื่อรัน query ผู้ใช้ที่ไม่ได้เข้าสู่ระบบและผู้ใช้ที่ไม่มี roles ที่จำเป็นจะไม่ได้รับอนุญาตให้เข้าถึง fields เหล่านี้