Bladeren bron

feat: AI 模型的术语读写一律必须落在 channel 内

此前模型建 studio 级术语是撞上 studio 归属校验才被拒的,报错含糊;改术语
则要等 owner 比对失败。现在显式判定身份:模型没给 channel 就直接 403,
并给出说得清的 message。

模型的权限全部由人类签出的 channel access token 代持,而 access token 是
channel 级的,代持不了 studio 权限——没有 channel 就没有任何可核验的授权。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 1 week geleden
bovenliggende
commit
dc0a333dff

+ 21 - 2
api-v13/app/Http/Controllers/DhammaTermController.php

@@ -7,6 +7,7 @@ use App\Http\Api\ShareApi;
 use App\Http\Api\StudioApi;
 use App\Http\Api\StudioApi;
 use App\Http\Controllers\Concerns\ChecksChannelEditPower;
 use App\Http\Controllers\Concerns\ChecksChannelEditPower;
 use App\Http\Resources\TermResource;
 use App\Http\Resources\TermResource;
+use App\Models\AiModel;
 use App\Models\Channel;
 use App\Models\Channel;
 use App\Models\DhammaTerm;
 use App\Models\DhammaTerm;
 use App\Services\AuthService;
 use App\Services\AuthService;
@@ -268,6 +269,12 @@ class DhammaTermController extends Controller
                     $term->language = $channelInfo['lang'];
                     $term->language = $channelInfo['lang'];
                 }
                 }
             } else {
             } else {
+                // AI 模型只能在 channel 里建术语。它的权限全部由人类签出的
+                // channel access token 代持,而 access token 是 channel 级的,
+                // 代持不了 studio 权限——没有 channel 就没有任何可核验的授权。
+                if ($this->isAiModel($user['user_uid'])) {
+                    return $this->error('ai model must specify a channel', [], 403);
+                }
                 if ($request->has('studioId')) {
                 if ($request->has('studioId')) {
                     $studioId = $request->input('studioId');
                     $studioId = $request->input('studioId');
                 } elseif ($request->has('studioName')) {
                 } elseif ($request->has('studioName')) {
@@ -299,6 +306,15 @@ class DhammaTermController extends Controller
         }
         }
     }
     }
 
 
+    /**
+     * 当前身份是不是 AI 模型。模型 token 的 user_id 恒为 0,但人类的旧 cookie
+     * 鉴权也可能给出奇怪的值,故直接查表判定,不靠 id。
+     */
+    private function isAiModel(string $userUid): bool
+    {
+        return AiModel::where('uid', $userUid)->exists();
+    }
+
     private function deleteCache($term)
     private function deleteCache($term)
     {
     {
         if (empty($term->channal)) {
         if (empty($term->channal)) {
@@ -348,8 +364,11 @@ class DhammaTermController extends Controller
         }
         }
 
 
         if (empty($dhammaTerm->channal)) {
         if (empty($dhammaTerm->channal)) {
-            // 查看有没有studio权限。access token 是 channel 级的,代持不了
-            // studio 权限,故 studio 级术语只有 owner 本人能改。
+            // studio 级术语(不属于任何 channel)只有 owner 本人能改:
+            // access token 是 channel 级的,代持不了 studio 权限。
+            if ($this->isAiModel($user['user_uid'])) {
+                return $this->error('ai model cannot edit a term outside a channel', [], 403);
+            }
             if ($user['user_uid'] !== $dhammaTerm->owner) {
             if ($user['user_uid'] !== $dhammaTerm->owner) {
                 return $this->error(__('auth.failed'), [], 403);
                 return $this->error(__('auth.failed'), [], 403);
             }
             }

+ 42 - 0
api-v13/tests/Feature/TermWriteAsAiModelTest.php

@@ -183,3 +183,45 @@ it('refuses a studio term written into someone elses studio', function () {
 
 
     expect(DhammaTerm::where('owner', $human)->count())->toBe(1);
     expect(DhammaTerm::where('owner', $human)->count())->toBe(1);
 });
 });
+
+it('refuses to create a studio-level term as an ai model', function () {
+    $human = makeStudio('tester');
+    $model = AiModel::factory()->ownedBy($human)->create();
+
+    $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
+        ->json('data.token');
+
+    // 不给 channel:模型没有任何可核验的授权,必须被拒
+    $this->postJson('/api/v2/terms', [
+        'word' => 'satipaṭṭhāna',
+        'meaning' => '念处',
+        'studioName' => 'tester',
+        'language' => 'zh-Hans',
+    ], ['Authorization' => 'Bearer '.$modelToken])
+        ->assertStatus(403);
+
+    expect(DhammaTerm::count())->toBe(0);
+});
+
+it('refuses to edit a studio-level term as an ai model', function () {
+    $human = makeStudio('tester');
+    $model = AiModel::factory()->ownedBy($human)->create();
+
+    // 人类先建一条 studio 级术语
+    $guid = $this->postJson('/api/v2/terms', [
+        'word' => 'satipaṭṭhāna',
+        'meaning' => '念处',
+        'studioName' => 'tester',
+        'language' => 'zh-Hans',
+    ], authHeader($human))->assertOk()->json('data.guid');
+
+    $modelToken = $this->getJson("/api/v2/ai-model-token/{$model->uid}", authHeader($human))
+        ->json('data.token');
+
+    $this->putJson("/api/v2/terms/{$guid}", [
+        'meaning' => '篡改',
+    ], ['Authorization' => 'Bearer '.$modelToken])
+        ->assertStatus(403);
+
+    expect(DhammaTerm::find($guid)->meaning)->toBe('念处');
+});