freetool-openfga-hexagonal-architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Freetool OpenFGA Hexagonal Architecture

Freetool OpenFGA 六边形架构

Overview

概述

Use this skill to produce repo-accurate guidance for OpenFGA in Freetool: where it is implemented, where it is not, and how to extend it without violating architecture boundaries.
使用本技能可以为Freetool中的OpenFGA实现提供符合仓库规范的指导:包括该在哪里实现、不该在哪里实现,以及如何在不违反架构边界的前提下扩展能力。

Workflow

工作流

  1. Read boundary-defining files first.
  2. Map each OpenFGA concern to API/Application/Infrastructure/Domain.
  3. Explain both positive placement ("implement here") and negative placement ("do not implement here").
  4. Include short F# snippets from the codebase that demonstrate best practice.
  5. Call out practical caveats and extension checklist.
  1. 优先读取边界定义文件
  2. 将每个OpenFGA相关需求映射到API/应用层/基础设施层/领域层
  3. 同时说明正向放置规则("此处实现")和反向禁止规则("此处禁止实现")
  4. 引入代码库中的简短F#代码片段作为最佳实践示例
  5. 列出实操注意事项和扩展检查清单

Read First

优先读取文件

  • src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
  • src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
  • src/Freetool.Api/src/Program.fs
  • src/Freetool.Api/src/Controllers/AuthenticatedControllerBase.fs
  • src/Freetool.Api/src/Controllers/SpaceController.fs
  • src/Freetool.Application/src/Handlers/SpaceHandler.fs
  • src/Freetool.Api/src/Services/IdentityProvisioningService.fs
  • src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
  • src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
  • src/Freetool.Api/src/Program.fs
  • src/Freetool.Api/src/Controllers/AuthenticatedControllerBase.fs
  • src/Freetool.Api/src/Controllers/SpaceController.fs
  • src/Freetool.Application/src/Handlers/SpaceHandler.fs
  • src/Freetool.Api/src/Services/IdentityProvisioningService.fs

Layer Mapping (Best Practice)

层级映射(最佳实践)

  • API layer (inbound adapters)
    : Middleware authenticates and sets
    UserId
    ; controllers enforce endpoint-level authorization checks and orchestration.
  • Application layer (ports + use cases)
    : Defines authorization port and typed auth language (
    IAuthorizationService
    ,
    AuthSubject
    ,
    AuthRelation
    ,
    AuthObject
    ); handlers orchestrate business use cases that need permission reads/writes.
  • Infrastructure layer (outbound adapter)
    : Implements OpenFGA SDK calls in
    OpenFgaService
    .
  • Domain layer
    : Contains zero OpenFGA/SDK knowledge; no external auth client details.
  • API层(入站适配器)
    :中间件完成身份认证并设置
    UserId
    ;控制器负责执行接口级权限校验和流程编排
  • 应用层(端口+用例)
    :定义授权端口和类型化权限语言(
    IAuthorizationService
    AuthSubject
    AuthRelation
    AuthObject
    );handler编排需要权限读写的业务用例
  • 基础设施层(出站适配器)
    :在
    OpenFgaService
    中实现OpenFGA SDK调用逻辑
  • 领域层
    :完全不感知OpenFGA/SDK相关内容,不包含任何外部权限客户端细节

Implement Here vs Do Not Implement Here

可实现区域 vs 禁止实现区域

Implement Here

可实现区域

  • Define authorization operations in
    IAuthorizationService
    (
    src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
    ).
  • Implement all OpenFGA SDK interactions in
    OpenFgaService
    (
    src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
    ).
  • Register the adapter in DI and initialize store/model in
    Program.fs
    .
  • Perform endpoint authorization checks in controllers (for request-scoped decisions).
  • Use application handlers for permission-diff orchestration and event coupling (for use-case-level permission changes).
  • IAuthorizationService
    src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
    )中定义授权操作
  • OpenFgaService
    src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
    )中实现所有OpenFGA SDK交互逻辑
  • Program.fs
    中注册适配器并初始化存储/模型
  • 在控制器中执行接口权限校验(用于请求级别的权限判断)
  • 在应用层handler中处理权限差异编排和事件耦合(用于用例级别的权限变更)

Do Not Implement Here

禁止实现区域

  • Do not call OpenFGA SDK directly from controllers, handlers, or domain.
  • Do not put OpenFGA tuple-string literals throughout API/domain logic; use typed auth unions and helpers.
  • Do not place authorization business policy inside domain entities.
  • Do not make Domain reference
    IAuthorizationService
    or infrastructure types.
  • 禁止在控制器、handler或领域层直接调用OpenFGA SDK
  • 禁止在API/领域逻辑中随处散落OpenFGA元组字符串字面量,使用类型化权限联合类型和辅助方法
  • 禁止将授权业务规则放在领域实体中
  • 禁止让领域层引用
    IAuthorizationService
    或基础设施层类型

Code Samples

代码示例

1) Application Port with Typed Auth Language

1) 带类型化权限语言的应用层端口

src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
fsharp
type IAuthorizationService =
    abstract member CreateRelationshipsAsync: RelationshipTuple list -> Task<unit>
    abstract member CheckPermissionAsync:
        subject: AuthSubject -> relation: AuthRelation -> object: AuthObject -> Task<bool>
src/Freetool.Application/src/Interfaces/IAuthorizationService.fs
fsharp
type IAuthorizationService =
    abstract member CreateRelationshipsAsync: RelationshipTuple list -> Task<unit>
    abstract member CheckPermissionAsync:
        subject: AuthSubject -> relation: AuthRelation -> object: AuthObject -> Task<bool>

2) Infrastructure Adapter Owns OpenFGA SDK

2) 基础设施层适配器托管OpenFGA SDK逻辑

src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
fsharp
open OpenFga.Sdk.Client

type OpenFgaService(apiUrl: string, logger: ILogger<OpenFgaService>, ?storeId: string) =
    interface IAuthorizationService with
        member _.CheckPermissionAsync(subject, relation, object) : Task<bool> =
            task {
                use client = createClient ()
                let body =
                    ClientCheckRequest(
                        User = AuthTypes.subjectToString subject,
                        Relation = AuthTypes.relationToString relation,
                        Object = AuthTypes.objectToString object
                    )
                let! response = client.Check(body)
                return response.Allowed.GetValueOrDefault(false)
            }
src/Freetool.Infrastructure/src/Services/OpenFgaService.fs
fsharp
open OpenFga.Sdk.Client

type OpenFGApiService(apiUrl: string, logger: ILogger<OpenFgaService>, ?storeId: string) =
    interface IAuthorizationService with
        member _.CheckPermissionAsync(subject, relation, object) : Task<bool> =
            task {
                use client = createClient ()
                let body =
                    ClientCheckRequest(
                        User = AuthTypes.subjectToString subject,
                        Relation = AuthTypes.relationToString relation,
                        Object = AuthTypes.objectToString object
                    )
                let! response = client.Check(body)
                return response.Allowed.GetValueOrDefault(false)
            }

3) DI Wiring Keeps API Depending on Port

3) DI 注入确保API层仅依赖端口

src/Freetool.Api/src/Program.fs
fsharp
builder.Services.AddScoped<IAuthorizationService>(fun serviceProvider ->
    let loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>()
    let logger = loggerFactory.CreateLogger<OpenFgaService>()
    OpenFgaService(openFgaApiUrl, logger, actualStoreId) :> IAuthorizationService)
|> ignore
src/Freetool.Api/src/Program.fs
fsharp
builder.Services.AddScoped<IAuthorizationService>(fun serviceProvider ->
    let loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>()
    let logger = loggerFactory.CreateLogger<OpenFgaService>()
    OpenFgaService(openFgaApiUrl, logger, actualStoreId) :> IAuthorizationService)
|> ignore

4) Controller Performs Endpoint Permission Check via Port

4) 控制器通过端口执行接口权限校验

src/Freetool.Api/src/Controllers/AppController.fs
fsharp
let! hasPermission =
    authorizationService.CheckPermissionAsync
        (User(userId.Value.ToString()))
        permission
        (SpaceObject(spaceId.Value.ToString()))
src/Freetool.Api/src/Controllers/AppController.fs
fsharp
let! hasPermission =
    authorizationService.CheckPermissionAsync
        (User(userId.Value.ToString()))
        permission
        (SpaceObject(spaceId.Value.ToString()))

5) Handler Performs Permission Diff + Atomic Tuple Update

5) Handler 执行权限差异对比 + 原子元组更新

src/Freetool.Application/src/Handlers/SpaceHandler.fs
fsharp
if not (List.isEmpty tuplesToAdd) || not (List.isEmpty tuplesToRemove) then
    do!
        authService.UpdateRelationshipsAsync
            { TuplesToAdd = tuplesToAdd
              TuplesToRemove = tuplesToRemove }
src/Freetool.Application/src/Handlers/SpaceHandler.fs
fsharp
if not (List.isEmpty tuplesToAdd) || not (List.isEmpty tuplesToRemove) then
    do!
        authService.UpdateRelationshipsAsync
            { TuplesToAdd = tuplesToAdd
              TuplesToRemove = tuplesToRemove }

Runtime Flow (Request to Decision)

运行时流程(从请求到权限决策)

  1. Middleware authenticates user and sets
    HttpContext.Items["UserId"]
    .
  2. AuthenticatedControllerBase
    reads
    CurrentUserId
    .
  3. Controller calls
    IAuthorizationService.CheckPermissionAsync
    .
  4. OpenFgaService
    converts typed values to OpenFGA tuple strings and calls SDK.
  5. Controller/handler continues or returns
    403
    .
  1. 中间件完成用户身份认证并设置
    HttpContext.Items["UserId"]
  2. AuthenticatedControllerBase
    读取
    CurrentUserId
  3. 控制器调用
    IAuthorizationService.CheckPermissionAsync
  4. OpenFgaService
    将类型化值转换为OpenFGA元组字符串并调用SDK
  5. 控制器/handler继续执行业务逻辑或返回
    403

Extension Checklist

扩展检查清单

When adding new authorization behavior:
  1. Add/adjust
    AuthRelation
    or typed auth models in
    IAuthorizationService.fs
    .
  2. Update relation-to-string mapping in
    AuthTypes
    .
  3. Update OpenFGA model definition in
    OpenFgaService.WriteAuthorizationModelAsync
    .
  4. Add use-case checks in controller and/or handler via
    IAuthorizationService
    .
  5. Add integration tests for allowed/denied paths and tuple-write behavior.
新增授权逻辑时:
  1. IAuthorizationService.fs
    中新增/调整
    AuthRelation
    或类型化权限模型
  2. 更新
    AuthTypes
    中的权限关系到字符串的映射逻辑
  3. OpenFgaService.WriteAuthorizationModelAsync
    中更新OpenFGA模型定义
  4. 通过
    IAuthorizationService
    在控制器和/或handler中新增用例级校验
  5. 为允许/拒绝路径和元组写入行为新增集成测试

Anti-Patterns to Flag in Review

代码评审中需要标记的反模式

  • open OpenFga.Sdk.*
    outside infrastructure.
  • Raw relation strings like
    "create_app"
    scattered in controllers/handlers.
  • Domain entities making auth service calls.
  • Controller bypassing typed
    AuthSubject/AuthRelation/AuthObject
    API.
  • 在基础设施层以外引入
    open OpenFga.Sdk.*
  • 控制器/handler中散落
    "create_app"
    这类原始权限关系字符串
  • 领域实体调用权限服务
  • 控制器绕过类型化
    AuthSubject/AuthRelation/AuthObject
    API直接使用原始值

Fast Verification Commands

快速校验命令

bash
rg -n "open OpenFga\\.Sdk|OpenFgaService|IAuthorizationService|CheckPermissionAsync|CreateRelationshipsAsync|UpdateRelationshipsAsync" src --glob '*.fs'
bash
rg -n "CurrentUserId|HttpContext\\.Items\\[\\\"UserId\\\"\\]|UseMiddleware<IapAuthMiddleware>|UseMiddleware<DevAuthMiddleware>" src/Freetool.Api/src --glob '*.fs'
bash
rg -n "open OpenFga\\.Sdk|OpenFgaService|IAuthorizationService|CheckPermissionAsync|CreateRelationshipsAsync|UpdateRelationshipsAsync" src --glob '*.fs'
bash
rg -n "CurrentUserId|HttpContext\\.Items\\[\\\"UserId\\\"\\]|UseMiddleware<IapAuthMiddleware>|UseMiddleware<DevAuthMiddleware>" src/Freetool.Api/src --glob '*.fs'