๐Ÿ›ก ๋ธ”๋ฆฌ์ธ  ๊ฐ€๋“œ

13103 ๋‹จ์–ด blitzjavascriptmonolithserverless
์ด ๊ธ€์€ ์›๋ž˜ Monolith Bias_ ๊ฐœ๋ฐœํŒ€์˜ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ Ingenious์— ๊ฒŒ์‹œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


Blitz๊ฐ€ ํ’€์Šคํƒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๊ฒƒ๋งŒํผ ํ›Œ๋ฅญํ•˜์ง€๋งŒ Ruby on Rails์— ํฌํ•จ๋œ ๋ฐฐํ„ฐ๋ฆฌ๊ฐ€ ์—ฌ์ „ํžˆ ๊ทธ๋ฆฝ์Šต๋‹ˆ๋‹ค. Rails์™€ ๊ฐ™์€ ์„ฑ์ˆ™ํ•œ ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ๊ฑฐ์˜ 1๋…„์ด ์ง€๋‚˜์ง€ ์•Š์€ Blitz์™€ ๋น„๊ตํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ์ง€๋งŒ "๊ทธ๋งŒํผ ๋ณด์„์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค"๋Š” ๋Š๋‚Œ์€ ํ™•์‹คํžˆ ๊ทธ๋ฆฝ์Šต๋‹ˆ๋‹ค.

์›น ์•ฑ์—์„œ ์ž‘์—…ํ•  ๋•Œ ๊ฐ€์žฅ ๊ทธ๋ฆฌ์›Œํ•˜๋Š” ๊ฒƒ ์ค‘ ํ•˜๋‚˜๋Š” ๊ถŒํ•œ ๋ถ€์—ฌ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ค‘์•™ ์ง‘์ค‘์‹ ์žฅ์†Œ์ž…๋‹ˆ๋‹ค. ์Šน์ธ์€ ์›น ์•ฑ์ด ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์š”๊ตฌ ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค. Blitz ์ž์ฒด๊ฐ€ ์„ธ์…˜์— ๋‚ด์žฅ๋œ $authorize ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜์ง€๋งŒ ๊ถŒํ•œ ๋ถ€์—ฌ๊ฐ€ ๋ฐ์ดํ„ฐ ์†์„ฑ์— ์ข…์†๋˜๋Š” ๊ฒฝ์šฐ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์ˆ ์ ์œผ๋กœ attributes-based access control .

์ข‹์€ ์†Œ์‹์€ ์นœ๊ตฌ์ด์ž ์ „ Ingenious ์ง์›์ด ๋งŒ๋“ Blitz Guard API๊ฐ€ RoR cancancan gem์— ๊ฐ€๊น์ง€๋งŒ Blitz์™€ ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋„๋ก ์กฐ์ •๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์„ค์น˜



Blitz Guard ์ตœ์‹  ๋ฆด๋ฆฌ์Šค์—์„œ๋Š” ๋‹ค์Œ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

$ blitz install ntgussoni/blitz-guard-recipe


์ด ๋ผ์ธ์€ Blitz Guard ๋ ˆ์‹œํ”ผ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค. Recipes์€ ์•ฑ์— ์ƒˆ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์„ค์น˜ํ•˜๋Š” ํ›Œ๋ฅญํ•œ ์•ˆ๋‚ด ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ์†์ƒ์‹œํ‚ค์ง€ ์•Š๊ณ  ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์•ˆ์— ๋ญ๊ฐ€ ๋“ค์–ด์žˆ์–ด



์ผ๋‹จ ์„ค์น˜ํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ƒˆ ํŒŒ์ผ๋กœ ๋๋‚ฉ๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ app/guard/ability.ts ์ž…๋‹ˆ๋‹ค. ์ด ํŒŒ์ผ์€ ๊ถŒํ•œ ๋ถ€์—ฌ ๋…ผ๋ฆฌ์˜ ํ•ต์‹ฌ์ด๋ฉฐ ์‚ฌ์šฉ์ž๊ฐ€ ์•ฑ์—์„œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€์— ๊ด€๊ณ„์—†์ด ๋‹จ์ผ ์ •๋ณด ์†Œ์Šค ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

import db from "db"
import { GuardBuilder, PrismaModelsType } from "@blitz-guard/core"
import { GetShoppingCartInput } from "app/shoppingCarts/queries/getShoppingCart"

type ExtendedResourceTypes = PrismaModelsType<typeof db>

type ExtendedAbilityTypes = ""

const Guard = GuardBuilder<ExtendedResourceTypes, ExtendedAbilityTypes>(
  async (ctx, { can, cannot }) => {
    cannot("manage", "all")
    if (ctx.session.$isAuthorized()) {
      can("read", "shoppingCart", async ({ where }: GetShoppingCartInput) => {
        return where.userId === ctx.session.userId
      })
    }
  }
)

export default Guard

ability ํŒŒ์ผ์€ Guard ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ GuardBuilder๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ๊ณผ ๊ฐ€๋“œ๋Š” ๋ชจ๋“  ์Šน์ธ๋œ ์ฟผ๋ฆฌ ๋˜๋Š” ๋Œ์—ฐ๋ณ€์ด์— ๋Œ€ํ•ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์†Œ์œ ์ž๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ๊ฑฐ๋ถ€ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// app/guard/ability.ts

import { GetShoppingCartInput } from "app/shoppingCart/queries/getShoppingCart"
// ...

if (ctx.session.$isAuthorized()) {
  can("read", "shoppingCart", async ( { where }: GetShoppingCartInput ) => {
    return where.userId === ctx.session.userId;
  })
}


GetShoppingCartInput์€ getShoppingCart ์ฟผ๋ฆฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ TS ์œ ํ˜•์ž…๋‹ˆ๋‹ค.
can (๋ฐ cannot ) ๋ฉ”์„œ๋“œ์—๋Š” ์„ธ ๊ฐœ์˜ ๋งค๊ฐœ ๋ณ€์ˆ˜๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ๋Š” ์ˆ˜ํ–‰ํ•  ์ž‘์—…(์ƒ์„ฑ, ์ฝ๊ธฐ, ์—…๋ฐ์ดํŠธ, ์‚ญ์ œ ๋˜๋Š” ๊ด€๋ฆฌ)์ด๊ณ , ๋‘ ๋ฒˆ์งธ๋Š” ์ด ๊ฐ€๋“œ๊ฐ€ ์ ์šฉ๋˜๋Š” Prisma ์Šคํ‚ค๋งˆ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค(์ด๋Š” Prisma์— ์ข…์†๋  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ ๋‹ค์Œ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ™•์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ExtendedResourceTypes ), ์„ธ ๋ฒˆ์งธ๋Š” ๋ถ€์šธ๋กœ ํ•ด์„๋˜์–ด์•ผ ํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.

์ด ์„ธ ๋ฒˆ์งธ ์ธ์ˆ˜์—์„œ๋Š” ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง€์ •๋œ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด DB ์ฟผ๋ฆฌ์™€ ๊ฐ™์€ ์›ํ•˜๋Š” ๋…ผ๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋” ํฅ๋ฏธ๋กœ์šด ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

//...
if (ctx.session.$isAuthorized()) {
  can("read", "shoppingCart", async ( { where }: GetShoppingCartInput ) => {
    if(where.userId === ctx.session.userId) return true
    const count = await db.shoppingCartShare.count({ where: { userId: ctx.session.userId, shoppingCartId: where.id } })
    return count > 0
  })
}


โ˜๏ธ ์—ฌ๊ธฐ์„œ ์šฐ๋ฆฌ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์†Œ์œ ์ž์ด๊ฑฐ๋‚˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ๊ฐ€ ์ด์ „์— ์ด ์‚ฌ์šฉ์ž์™€ ๊ณต์œ ๋˜์—ˆ๋‹ค๊ณ  ์ฃผ์žฅํ•ฉ๋‹ˆ๋‹ค.

๊ถŒํ•œ ๋ถ€์—ฌ ๊ธฐ๋Šฅ



์ง€๊ธˆ๊นŒ์ง€๋Š” ๋„ˆ๋ฌด ์ข‹์•˜์ง€๋งŒ ๊ธฐ๋Šฅ์„ ์Šน์ธํ•˜์ง€ ์•Š์œผ๋ฉด ๋Šฅ๋ ฅ ํŒŒ์ผ์„ ๋ณ€๊ฒฝํ•ด๋„ ์•„๋ฌด ์†Œ์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค. Blitz Guard๋Š” authorize ๋ฉ”์„œ๋“œ๋กœ ์ฟผ๋ฆฌ์™€ ๋Œ์—ฐ๋ณ€์ด๋ฅผ ๋ž˜ํ•‘ํ•˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ๊ฐ€๋กœ์ฑ„์„œ ๊ฐ€๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

import { Ctx, NotFoundError } from "blitz"
import db, { Prisma } from "db"
import Guard from "app/guard/ability"

export type GetShoppingCartInput = Pick<Prisma.ShoppingCartFindFirstArgs, "where">

async function getShoppingCart({ where }: GetShoppingCartInput, ctx: Ctx) {
 ctx.session.$authorize()

 const cart = await db.shoppingCart.findFirst({ where })

 if (!cart) throw new NotFoundError()

 return cart
}

export default Guard.authorize("read", "shoppingCart", getShoppingCart)


์ด ํ•จ์ˆ˜์˜ ์ฒ˜์Œ ๋‘ ์ธ์ˆ˜๋Š” can ๋ฐ cannot ํ•จ์ˆ˜๊ฐ€ ๋ฐ›๋Š” ๋™์ผํ•œ ์ธ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ์นœ์ˆ™ํ•ด ๋ณด์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์„ธ ๋ฒˆ์งธ ์ธ์ˆ˜๋Š” ์šฐ๋ฆฌ๊ฐ€ ๊ฐ์‹ธ๊ณ ์ž ํ•˜๋Š” ํ•จ์ˆ˜์ด๋ฉฐ, ๊ฐ€๋“œ ๊ธฐ์ค€์ด ์ถฉ์กฑ๋  ๋•Œ๋งŒ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด 403์ด ๋ฉ๋‹ˆ๋‹ค.

์ด ๋‹จ๊ณ„๋Š” ์žŠ์–ด๋ฒ„๋ฆฌ๊ธฐ ์‰ฝ๊ณ , Blitz ์ƒ์„ฑ๊ธฐ ๊ธฐ๋ณธ๊ฐ’์ด ์ƒ์„ฑ๋œ ๊ธฐ๋Šฅ์„ ๋‚ด๋ณด๋‚ผ ๋•Œ ๋”์šฑ ๊ทธ๋ ‡์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๋•๊ธฐ ์œ„ํ•ด Blitz Guard๋Š” Guard.authorize ํ•จ์ˆ˜๋กœ ๋ž˜ํ•‘๋˜์ง€ ์•Š์€ ์ฟผ๋ฆฌ ๋ฐ ๋ณ€ํ˜•์— ๋Œ€ํ•ด (๊ฐœ๋ฐœ ์ค‘) ๊ฒฝ๊ณ ํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์„ค์น˜ํ•ฉ๋‹ˆ๋‹ค.

๋งˆ์ง€๋ง‰ ๋‹จ์–ด



Blitz Guard๋Š” ์•„์ง ๋งŽ์ด ๊ฐœ๋ฐœ ์ค‘์ด์ง€๋งŒ API๊ฐ€ ๋งŽ์ด ๋ฐ”๋€” ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํฉ์–ด์ ธ ์žˆ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํ•œ ๊ณณ์œผ๋กœ ์˜ฎ๊ฒจ ์•ผ์‹ฌ์ฐฌ Blitz ์•ฑ์„ ๊ฐœ๋ฐœํ•  ๊ณ„ํš์ด๋ผ๋ฉด ํ›Œ๋ฅญํ•œ ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ