Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

entのPrivacy機能とgo/astを使って、意図しないDBアクセスを防ぐ

Avatar for KazukiHayase KazukiHayase
December 05, 2025
130

 entのPrivacy機能とgo/astを使って、意図しないDBアクセスを防ぐ

Avatar for KazukiHayase

KazukiHayase

December 05, 2025
Tweet

More Decks by KazukiHayase

Transcript

  1. 自己紹介 @KazukiHayase 早瀬和輝(Kazuki Hayase) 2025年4月ドクターズプライム入社 リードエンジニア Go / TypeScript /

    React(Next.js) / GraphQL ダイエットのためにラーメン制限中 ©2025 Dr.'s Prime ,Inc.
  2. 背景 一般的なアプローチ Repository層やUseCase層でフィルタリング条件を追加 func (r *itemRepository) List(ctx context.Context, userID string)

    ([]*Item, error) { var items []*Item err := r.db. Joins("JOIN companies ON items.company_id = companies.id"). // 中間テーブルでブロックされていないItemを取得 Where("NOT EXISTS (SELECT 1 FROM blocked_users WHERE company_id = companies.id AND user_id = ?)", userID). Find(&items) if err != nil { return nil, err } return items, nil } → 実装者がフィルタリングを忘れる可能性がある ©2025 Dr.'s Prime ,Inc.
  3. entのprivacy機能の活用 privacy機能の利点 公式ドキュメントより引用 The main advantage of the privacy layer

    is that, you write the privacy policy once (in the schema), and it is always evaluated. No matter where queries and mutations are performed in your codebase, it will always go through the privacy layer. → 一度定義すれば、どの経路でも必ずフィルタリングが適用される ©2025 Dr.'s Prime ,Inc.
  4. entのprivacy機能の活用 スキーマでのポリシー定義 func (Item) Policy() ent.Policy { return privacy.Policy{ Mutation:

    privacy.MutationPolicy{ // 全ての変更操作を許可(デフォルト) privacy.AlwaysAllowRule(), }, Query: privacy.QueryPolicy{ // カスタムフィルタリングルールを適用 rule.FilterBlockedUserRule(), }, } } ©2025 Dr.'s Prime ,Inc.
  5. entのprivacy機能の活用 フィルタリングルールの実装 func FilterBlockedUserRule() privacy.QueryRule { return privacy.ItemQueryRuleFunc(func(ctx context.Context, q

    *ent.ItemQuery) error { userID := ctxutil.UserID(ctx) if userID == "" { return privacy.Denyf("no user ID in context") } // ブロックされているユーザーのアイテムを除外するWhere句を付与 q.Where( item.Not( item.HasCompanyWith( company.HasBlockedUsersWith( blockeduser.UserIDEQ(userID), ), ), ), ) return privacy.Skip }) } ©2025 Dr.'s Prime ,Inc.
  6. go/astを使った静的解析 Repository層での対応 contextを設定するプライベートメソッドを用意 func (repo *itemRepository) List(ctx context.Context, userID string)

    ([]*Item, error) { ctx = repo.setContext(ctx, userID) // contextを設定 // ...クエリを実行 } func (repo *itemRepository) setContext(ctx context.Context, userID string) context.Context { return ctxutil.SetUserID(ctx, userID) } ©2025 Dr.'s Prime ,Inc.
  7. go/astを使った静的解析 テストコードの例 func TestItemRepositoryMethodCallCheck(t *testing.T) { const ( targetStructName =

    "itemRepository" targetSourceFileName = "item_repository.go" targetMethodName = "setContext" ) // AST解析で各メソッドがsetContextを呼んでいるかチェック gotCallMap, err := CheckMethodCallsInStruct(targetSourceFileName, targetStructName, targetMethodName) if err != nil { t.Fatal(err) } wantCallMap := map[string]bool{ "List": true, "GetByID": true, } if !reflect.DeepEqual(wantCallMap, gotCallMap) { t.Errorf("Method list mismatch.\nWanted: %v\nGot: %v", wantCallMap, gotCallMap) } } ©2025 Dr.'s Prime ,Inc.
  8. go/astを使った静的解析 AST解析の実装(1/3) func CheckMethodCallsInStruct(targetFileName, targetStructName, targetMethodName string) (map[string]bool, error) {

    // ソースファイルをパース fset := token.NewFileSet() node, err := parser.ParseFile(fset, targetFileName, nil, 0) if err != nil { return nil, err } // ASTを走査して、targetStructNameのメソッドを全て抽出し、 // targetMethodNameが呼び出されているかをcallMapに格納する callMap := make(map[string]bool) ast.Inspect(node, func(n ast.Node) bool { // 次のスライドに続く... }) return callMap, nil } ©2025 Dr.'s Prime ,Inc.
  9. go/astを使った静的解析 AST解析の実装(2/3) ast.Inspect(node, func(n ast.Node) bool { // メソッド宣言のみ対象(レシーバーがない関数は除外) funcDecl,

    ok := n.(*ast.FuncDecl) if !ok || funcDecl.Recv == nil { return true } // レシーバーの型名を取得 var recvTypeName string switch recv := funcDecl.Recv.List[0].Type.(type) { case *ast.StarExpr: if ident, ok := recv.X.(*ast.Ident); ok { recvTypeName = ident.Name } case *ast.Ident: recvTypeName = recv.Name } if recvTypeName != targetStructName { return true // 対象の構造体でない場合はスキップ } // 次のスライドに続く... }) ©2025 Dr.'s Prime ,Inc.
  10. go/astを使った静的解析 AST解析の実装(3/3) ast.Inspect(node, func(n ast.Node) bool { // ...前のスライドから続く methodName

    := funcDecl.Name.Name // targetMethodNameの呼び出しをチェック isCalledTargetMethod := false ast.Inspect(funcDecl.Body, func(n ast.Node) bool { callExpr, ok := n.(*ast.CallExpr) if !ok { return true } selExpr, ok := callExpr.Fun.(*ast.SelectorExpr) if !ok { return true } if selExpr.Sel.Name == targetMethodName { isCalledTargetMethod = true return false } return true }) callMap[methodName] = isCalledTargetMethod return true }) ©2025 Dr.'s Prime ,Inc.
  11. go/astを使った静的解析 メソッド追加時の例 setContext の呼び出しが漏れているメソッドを追加した場合 type itemRepository struct {} // ...他のメソッド

    // 新規メソッド追加 func (repo *itemRepository) GetMulti(ctx context.Context, ids []string) ([]*Item, error) { return []*Item{}, nil } ©2025 Dr.'s Prime ,Inc.
  12. go/astを使った静的解析 メソッド追加時の例 テストを実行すると GetMulti が wantCallMap に存在しないので失敗 ❯ go test

    ./... --- FAIL: TestItemRepositoryMethodCallCheck (0.00s) item_repository_test.go:30: Method list mismatch. Wanted: map[GetByID:true List:true] Got: map[GetByID:true GetMulti:false List:true] FAIL FAIL sample 0.251s FAIL ©2025 Dr.'s Prime ,Inc.