refactor: OAuth2 代理人重构 · Service Account→OAuth2 全量替换 · ZY-OAUTH2-REBUILD-2026-0324-001 [skip ci]

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/75c92776-b0de-4ee6-86c0-9e774d5ec717
This commit is contained in:
copilot-swe-agent[bot] 2026-03-24 07:25:56 +00:00
parent 368a7f139f
commit 7d6122a5c8
13 changed files with 173 additions and 318 deletions

View File

@ -21,7 +21,9 @@ jobs:
- name: ⚡ 执行部署 - name: ⚡ 执行部署
env: env:
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
DEPLOY_GITHUB_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} DEPLOY_GITHUB_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }}
run: | run: |
for f in grid-db/deploy-queue/*.json; do for f in grid-db/deploy-queue/*.json; do

View File

@ -1,18 +1,18 @@
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit) # 🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)
# #
# 守护: PER-ZY001 铸渊 (Zhuyuan) # 守护: PER-ZY001 铸渊 (Zhuyuan)
# 系统: SYS-GLW-0001 # 系统: SYS-GLW-0001
# 主控: TCS-0002∞ (冰朔) # 主控: TCS-0002∞ (冰朔)
# #
# 功能: # 功能:
# ① GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性校验 # ① OAuth2 凭据环境变量完整性校验
# ② 依赖图扫描(工作流 + 脚本) # ② 依赖图扫描(工作流 + 脚本)
# ③ YAML 语法全量扫描 # ③ YAML 语法全量扫描
# ④ 审计报告归档至 System_Logs/ # ④ 审计报告归档至 System_Logs/
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
name: "🛰️ 天眼密钥流审计 (Sky-Eye Credential Audit)" name: "🛰️ 天眼 OAuth2 凭据审计 (Sky-Eye Credential Audit)"
on: on:
schedule: schedule:
@ -32,7 +32,9 @@ jobs:
- name: "🛰️ Run Sky-Eye Credential Audit" - name: "🛰️ Run Sky-Eye Credential Audit"
env: env:
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
run: node scripts/skyeye/credential-audit.js run: node scripts/skyeye/credential-audit.js
- name: "📝 Commit audit report" - name: "📝 Commit audit report"

View File

@ -26,7 +26,9 @@ jobs:
- name: 🪞 同步到 Google Drive - name: 🪞 同步到 Google Drive
env: env:
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
DRIVE_FOLDER_ID: ${{ secrets.DRIVE_MIRROR_FOLDER_ID }} DRIVE_FOLDER_ID: ${{ secrets.DRIVE_MIRROR_FOLDER_ID }}
run: node scripts/grid-db/sync-to-drive.js run: node scripts/grid-db/sync-to-drive.js

View File

@ -5,8 +5,8 @@
# 系统: SYS-GLW-0001 # 系统: SYS-GLW-0001
# 主控: TCS-0002∞ (冰朔) # 主控: TCS-0002∞ (冰朔)
# #
# 方案: 宿主机直装 rclone替代 wei/rclone 容器方案 # 方案: 宿主机直装 rcloneOAuth2 代理人模式
# 凭证: GOOGLE_DRIVE_SERVICE_ACCOUNT (Service Account JSON) # 凭证: GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
# 目标: Google Drive 文件夹 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G # 目标: Google Drive 文件夹 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@ -36,39 +36,32 @@ jobs:
sudo apt-get install -y -qq rclone sudo apt-get install -y -qq rclone
rclone version rclone version
- name: "🔑 Configure rclone (Service Account)" - name: "🔑 Configure rclone (OAuth2)"
env: env:
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
run: | run: |
if [ -z "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" ]; then if [ -z "${GDRIVE_CLIENT_ID}" ] || [ -z "${GDRIVE_CLIENT_SECRET}" ] || [ -z "${GDRIVE_REFRESH_TOKEN}" ]; then
echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT secret is not set" echo "::error::OAuth2 secrets not set (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)"
exit 1 exit 1
fi fi
SA_FILE="$(mktemp "${RUNNER_TEMP}/gd-sa-XXXXXX.json")" echo "✅ OAuth2 credentials validated"
printf '%s\n' "${GOOGLE_DRIVE_SERVICE_ACCOUNT}" > "${SA_FILE}"
chmod 600 "${SA_FILE}"
# 天眼密钥流校验:验证 JSON 完整性
if ! node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "${SA_FILE}" 2>/dev/null; then
echo "::error::GOOGLE_DRIVE_SERVICE_ACCOUNT contains invalid JSON (possible truncation)"
rm -f "${SA_FILE}"
exit 1
fi
echo "✅ Service account JSON validated"
mkdir -p "${HOME}/.config/rclone" mkdir -p "${HOME}/.config/rclone"
{ {
echo "[gdrive]" echo "[gdrive]"
echo "type = drive" echo "type = drive"
echo "scope = drive" echo "scope = drive"
echo "service_account_file = ${SA_FILE}" echo "client_id = ${GDRIVE_CLIENT_ID}"
echo "client_secret = ${GDRIVE_CLIENT_SECRET}"
echo "token = {\"access_token\":\"\",\"token_type\":\"Bearer\",\"refresh_token\":\"${GDRIVE_REFRESH_TOKEN}\",\"expiry\":\"2000-01-01T00:00:00Z\"}"
echo "root_folder_id = 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G" echo "root_folder_id = 1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G"
} > "${HOME}/.config/rclone/rclone.conf" } > "${HOME}/.config/rclone/rclone.conf"
chmod 600 "${HOME}/.config/rclone/rclone.conf" chmod 600 "${HOME}/.config/rclone/rclone.conf"
echo "SA_FILE=${SA_FILE}" >> "${GITHUB_ENV}" echo "✅ rclone configured with OAuth2"
echo "✅ rclone configured with Service Account"
- name: "🪞 Sync to Google Drive" - name: "🪞 Sync to Google Drive"
# 使用 copy非 sync—— 仅添加/更新文件,不删除 Drive 端已有文件(防误删) # 使用 copy非 sync—— 仅添加/更新文件,不删除 Drive 端已有文件(防误删)
@ -90,6 +83,5 @@ jobs:
- name: "🧹 Cleanup credentials" - name: "🧹 Cleanup credentials"
if: always() if: always()
run: | run: |
rm -f "${SA_FILE}"
rm -f "${HOME}/.config/rclone/rclone.conf" rm -f "${HOME}/.config/rclone/rclone.conf"
echo "✅ Credentials cleaned up" echo "✅ Credentials cleaned up"

View File

@ -31,7 +31,9 @@ jobs:
- name: 🛰️ 执行语义落盘 - name: 🛰️ 执行语义落盘
env: env:
GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GDRIVE_CLIENT_ID: ${{ secrets.GDRIVE_CLIENT_ID }}
GDRIVE_CLIENT_SECRET: ${{ secrets.GDRIVE_CLIENT_SECRET }}
GDRIVE_REFRESH_TOKEN: ${{ secrets.GDRIVE_REFRESH_TOKEN }}
DRIVE_LANDING_FOLDER_ID: '1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G' DRIVE_LANDING_FOLDER_ID: '1fqWdLPaZkUZYt4OT_h3fJQHnd-q5QN3G'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }} ISSUE_NUMBER: ${{ github.event.issue.number }}

View File

@ -28,11 +28,11 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
### A.1 Google Cloud 配置 ### A.1 Google Cloud 配置
1. 前往 [Google Cloud Console](https://console.cloud.google.com) 1. 前往 [Google Cloud Console](https://console.cloud.google.com)
2. 创建项目或使用已有项目 2. 创建项目或使用已有项目(项目名:`guanghu-drive-bridge`
3. 启用 **Google Drive API** 3. 启用 **Google Drive API**、**Google Docs API**、**Google Sheets API**
4. 创建 **Service Account**,下载 JSON 密钥文件 4. 创建 **OAuth 2.0 客户端凭据**(桌面应用类型)
5. 在用户的 Google Drive 中创建「光湖格点库」文件夹 5. 完成 OAuth 授权流程,获取 Refresh Token
6. 将 Service Account 邮箱分享到该文件夹(**编辑者**权限) 6. 在用户的 Google Drive 中创建「光湖格点库」文件夹
### A.2 配置 GitHub Secrets ### A.2 配置 GitHub Secrets
@ -40,7 +40,9 @@ Gemini 无法直接读写 GitHub 仓库文件。本方案通过 Google Drive 作
| Secret 名称 | 值 | | Secret 名称 | 值 |
|---|---| |---|---|
| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥的完整内容 | | `GDRIVE_CLIENT_ID` | OAuth 客户端 ID |
| `GDRIVE_CLIENT_SECRET` | OAuth 客户端密钥 |
| `GDRIVE_REFRESH_TOKEN` | OAuth 长效刷新令牌 |
| `DRIVE_MIRROR_FOLDER_ID` | Drive「光湖格点库」文件夹的 IDURL 中最后一段) | | `DRIVE_MIRROR_FOLDER_ID` | Drive「光湖格点库」文件夹的 IDURL 中最后一段) |
### A.3 自动触发 ### A.3 自动触发
@ -109,7 +111,7 @@ Gemini 通过 Personal Context 读取 Drive `mirror/` 中的文件,通过在 `
## 安全注意事项 ## 安全注意事项
- **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码 - **GITHUB_TOKEN** 必须存在 Apps Script 的 Script Properties 中,禁止硬编码
- **Service Account** 密钥只存在 GitHub Secrets 中,不入库 - **OAuth2 凭据** 只存在 GitHub Secrets 中,不入库
- Drive 中的所有文件都是副本,丢失可从仓库重新同步 - Drive 中的所有文件都是副本,丢失可从仓库重新同步
- Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/` - Apps Script 只处理 `inbox/` 文件夹,不碰 `mirror/`
- 所有自动提交包含 `[skip ci]` 防止循环触发 - 所有自动提交包含 `[skip ci]` 防止循环触发
@ -164,6 +166,8 @@ Schema 见 `grid-db/schema/deploy-command.schema.json`。
| Secret | 用途 | | Secret | 用途 |
|---|---| |---|---|
| `GOOGLE_DRIVE_SERVICE_ACCOUNT` | Service Account JSON 密钥 | | `GDRIVE_CLIENT_ID` | OAuth 客户端 ID |
| `GDRIVE_CLIENT_SECRET` | OAuth 客户端密钥 |
| `GDRIVE_REFRESH_TOKEN` | OAuth 长效刷新令牌 |
| `DRIVE_MIRROR_FOLDER_ID` | 镜像同步目标文件夹 ID | | `DRIVE_MIRROR_FOLDER_ID` | 镜像同步目标文件夹 ID |
| `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Tokenrepo 权限) | | `DEPLOY_GITHUB_TOKEN` | 部署脚本使用的 GitHub Tokenrepo 权限) |

View File

@ -4,7 +4,7 @@
* 功能一键部署 / 一键恢复 Drive 桥接层 * 功能一键部署 / 一键恢复 Drive 桥接层
* *
* 读取 grid-db/deploy-queue/*.json 中的部署指令执行 * 读取 grid-db/deploy-queue/*.json 中的部署指令执行
* Service Account 在用户 Drive 创建光湖格点库目录结构 * OAuth2 代理人在用户 Drive 创建光湖格点库目录结构
* 从仓库同步初始数据到 Drive mirror/ * 从仓库同步初始数据到 Drive mirror/
* 生成用户专属的 index.json DEV 编号和人格体信息 * 生成用户专属的 index.json DEV 编号和人格体信息
* 生成 Gemini 启动指令 写入 Drive * 生成 Gemini 启动指令 写入 Drive
@ -14,7 +14,9 @@
* 用法node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json> * 用法node scripts/grid-db/deploy-drive-bridge.js <deploy-command.json>
* *
* 环境变量 * 环境变量
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容 * - GDRIVE_CLIENT_ID: OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
* - DEPLOY_GITHUB_TOKEN: 具有 repo 权限的 GitHub Token用于配置 Apps Script * - DEPLOY_GITHUB_TOKEN: 具有 repo 权限的 GitHub Token用于配置 Apps Script
* *
* 守护: PER-ZY001 铸渊 * 守护: PER-ZY001 铸渊
@ -296,41 +298,17 @@ async function deploy(commandFilePath) {
console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`); console.log(`[deploy] ${action} for ${dev_id} (${dev_name}) → ${google_email}`);
// 验证环境变量 // OAuth2 认证(统一入口)
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; let drive;
if (!serviceAccountJson) { try {
writeReceipt(deploy_id, dev_id, 'failed', 'GOOGLE_DRIVE_SERVICE_ACCOUNT not set'); const { getDriveClient } = require('./drive-auth');
drive = getDriveClient();
console.log('[deploy] ✅ OAuth2 credentials configured');
} catch (err) {
writeReceipt(deploy_id, dev_id, 'failed', `OAuth2 auth failed: ${err.message}`);
return; return;
} }
// 认证 Google Drive API天眼密钥流校验
let credentials;
try {
const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator');
const validation = validateServiceAccountJSON(serviceAccountJson);
if (!validation.valid) {
console.error('[deploy] 🔴 Credential validation failed:');
console.error(formatDiagnosticReport(validation));
writeReceipt(deploy_id, dev_id, 'failed', 'Credential validation failed: invalid service account JSON format');
return;
}
credentials = validation.credentials;
console.log('[deploy] ✅ Service account credentials validated');
} catch (err) {
// Fallback: if validator module is unavailable, try direct parse
try {
credentials = JSON.parse(serviceAccountJson);
} catch (parseErr) {
writeReceipt(deploy_id, dev_id, 'failed', `JSON parse error: ${parseErr.message}`);
return;
}
}
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/drive']
});
const drive = google.drive({ version: 'v3', auth });
try { try {
// ① 在用户 Drive 创建或复用根目录 // ① 在用户 Drive 创建或复用根目录
const rootFolderName = config.drive_root_folder || '光湖格点库'; const rootFolderName = config.drive_root_folder || '光湖格点库';

View File

@ -0,0 +1,47 @@
/**
* scripts/grid-db/drive-auth.js
*
* OAuth2 认证模块 · 替代原 Service Account 模式
*
* 环境变量
* GDRIVE_CLIENT_ID OAuth 客户端 ID
* GDRIVE_CLIENT_SECRET OAuth 客户端密钥
* GDRIVE_REFRESH_TOKEN 长效刷新令牌
*
* 返回已认证的 google.drive 客户端实例
*
* 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001
* 主控: TCS-0002 冰朔
*/
const { google } = require('googleapis');
function getOAuth2Client() {
const clientId = process.env.GDRIVE_CLIENT_ID;
const clientSecret = process.env.GDRIVE_CLIENT_SECRET;
const refreshToken = process.env.GDRIVE_REFRESH_TOKEN;
if (!clientId || !clientSecret || !refreshToken) {
const missing = [];
if (!clientId) missing.push('GDRIVE_CLIENT_ID');
if (!clientSecret) missing.push('GDRIVE_CLIENT_SECRET');
if (!refreshToken) missing.push('GDRIVE_REFRESH_TOKEN');
throw new Error(`Missing OAuth2 environment variables: ${missing.join(', ')}`);
}
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret);
oauth2Client.setCredentials({
refresh_token: refreshToken
});
return oauth2Client;
}
function getDriveClient() {
const auth = getOAuth2Client();
return google.drive({ version: 'v3', auth });
}
module.exports = { getDriveClient, getOAuth2Client };

View File

@ -19,7 +19,9 @@
* - 不同步 inbox/ processing/系统内部流转区 * - 不同步 inbox/ processing/系统内部流转区
* *
* 环境变量 * 环境变量
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥内容 * - GDRIVE_CLIENT_ID: OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
* - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID * - DRIVE_FOLDER_ID: Drive 镜像根文件夹 ID
* *
* 守护: PER-ZY001 铸渊 * 守护: PER-ZY001 铸渊
@ -169,48 +171,24 @@ async function uploadFile(drive, folderId, fileName, content) {
*/ */
async function main() { async function main() {
// 验证环境变量 // 验证环境变量
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
const rootFolderId = process.env.DRIVE_FOLDER_ID; const rootFolderId = process.env.DRIVE_FOLDER_ID;
if (!serviceAccountJson) {
console.log('[sync-to-drive] GOOGLE_DRIVE_SERVICE_ACCOUNT not set, skipping');
return;
}
if (!rootFolderId) { if (!rootFolderId) {
console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping'); console.log('[sync-to-drive] DRIVE_FOLDER_ID not set, skipping');
return; return;
} }
// 解析 Service Account 凭证(天眼密钥流校验 // OAuth2 认证(统一入口
let credentials; let drive;
try { try {
const { validateServiceAccountJSON, formatDiagnosticReport } = require('../skyeye/credential-validator'); const { getDriveClient } = require('./drive-auth');
const validation = validateServiceAccountJSON(serviceAccountJson); drive = getDriveClient();
if (!validation.valid) { console.log('[sync-to-drive] ✅ OAuth2 credentials configured');
console.error('[sync-to-drive] 🔴 Credential validation failed:');
console.error(formatDiagnosticReport(validation));
process.exit(1);
}
credentials = validation.credentials;
console.log('[sync-to-drive] ✅ Service account credentials validated');
} catch (err) { } catch (err) {
// Fallback: if validator module is unavailable, try direct parse console.error(`[sync-to-drive] 🔴 OAuth2 auth failed: ${err.message}`);
try { process.exit(1);
credentials = JSON.parse(serviceAccountJson);
} catch (parseErr) {
console.error('[sync-to-drive] Failed to parse service account JSON:', parseErr.message);
process.exit(1);
}
} }
// 认证 Google Drive API
const auth = new google.auth.GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/drive']
});
const drive = google.drive({ version: 'v3', auth });
// 预生成 Drive 索引文件 // 预生成 Drive 索引文件
try { try {
const { main: generateIndex } = require('./generate-drive-index'); const { main: generateIndex } = require('./generate-drive-index');

View File

@ -5,13 +5,15 @@
* 天眼全域系统审计 (Sky-Eye Credential Audit) * 天眼全域系统审计 (Sky-Eye Credential Audit)
* *
* 功能 * 功能
* 校验 GOOGLE_DRIVE_SERVICE_ACCOUNT 密钥流完整性 * 校验 OAuth2 凭据环境变量完整性
* 扫描所有依赖该密钥的工作流和脚本 * 扫描所有依赖 Drive 凭据的工作流和脚本
* 验证 YAML 语法.github/workflows/ 下所有配置文件 * 验证 YAML 语法.github/workflows/ 下所有配置文件
* 生成审计报告写入 System_Logs/ * 生成审计报告写入 System_Logs/
* *
* 环境变量 * 环境变量
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: (可选) 用于实际校验 * - GDRIVE_CLIENT_ID: (可选) OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: (可选) OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: (可选) 长效刷新令牌
* *
* 用法node scripts/skyeye/credential-audit.js * 用法node scripts/skyeye/credential-audit.js
* *
@ -24,7 +26,7 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { validateServiceAccountJSON, formatDiagnosticReport } = require('./credential-validator'); const { validateOAuth2Credentials, formatDiagnosticReport } = require('./credential-validator');
const ROOT = path.resolve(__dirname, '../..'); const ROOT = path.resolve(__dirname, '../..');
const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows'); const WORKFLOWS_DIR = path.join(ROOT, '.github/workflows');
@ -43,24 +45,20 @@ function getDateStr() {
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
function auditCredential() { function auditCredential() {
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; const hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
if (!serviceAccountJson) { if (!hasAnyVar) {
return { return {
status: 'skipped', status: 'skipped',
reason: 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available in environment', reason: 'OAuth2 credentials not available in environment (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)',
recommendation: 'Run this audit in a GitHub Actions workflow with the secret configured' recommendation: 'Run this audit in a GitHub Actions workflow with the secrets configured'
}; };
} }
const validation = validateServiceAccountJSON(serviceAccountJson); const validation = validateOAuth2Credentials();
return { return {
status: validation.valid ? 'pass' : 'fail', status: validation.valid ? 'pass' : 'fail',
diagnostics: { diagnostics: validation.diagnostics,
...validation.diagnostics,
// Never expose credential values in reports
credentials: undefined
},
issues: validation.issues, issues: validation.issues,
report: formatDiagnosticReport(validation) report: formatDiagnosticReport(validation)
}; };
@ -77,7 +75,7 @@ function scanCredentialDependencies() {
total_dependents: 0 total_dependents: 0
}; };
const SEARCH_PATTERN = 'GOOGLE_DRIVE_SERVICE_ACCOUNT'; const SEARCH_PATTERN = 'GDRIVE_CLIENT_ID';
// 扫描 workflows // 扫描 workflows
if (fs.existsSync(WORKFLOWS_DIR)) { if (fs.existsSync(WORKFLOWS_DIR)) {

View File

@ -2,15 +2,15 @@
/** /**
* scripts/skyeye/credential-validator.js * scripts/skyeye/credential-validator.js
* *
* 天眼密钥流完整性校验器 (Sky-Eye Credential Validator) * 天眼 OAuth2 凭据校验器 (Sky-Eye Credential Validator)
* *
* 功能 Google Drive Service Account JSON 执行结构化检测 * 功能 Google Drive OAuth2 环境变量执行存在性检测
* - JSON 语法完整性检测未闭合 {} / 截断 * - GDRIVE_CLIENT_ID 存在性
* - 必需字段存在性验证 * - GDRIVE_CLIENT_SECRET 存在性
* - 字段值格式校验private_key PEM 格式等 * - GDRIVE_REFRESH_TOKEN 存在性
* *
* 可作为模块 require() 使用也可直接运行 * 可作为模块 require() 使用也可直接运行
* GOOGLE_DRIVE_SERVICE_ACCOUNT='...' node credential-validator.js * GDRIVE_CLIENT_ID='...' GDRIVE_CLIENT_SECRET='...' GDRIVE_REFRESH_TOKEN='...' node credential-validator.js
* *
* 守护: PER-ZY001 铸渊 * 守护: PER-ZY001 铸渊
* 系统: SYS-GLW-0001 * 系统: SYS-GLW-0001
@ -19,147 +19,46 @@
'use strict'; 'use strict';
// Google Service Account JSON 必需字段 // OAuth2 必需环境变量
const REQUIRED_FIELDS = [ const REQUIRED_OAUTH2_VARS = [
'type', 'GDRIVE_CLIENT_ID',
'project_id', 'GDRIVE_CLIENT_SECRET',
'private_key_id', 'GDRIVE_REFRESH_TOKEN'
'private_key',
'client_email',
'client_id',
'auth_uri',
'token_uri'
]; ];
/** /**
* 验证 Service Account JSON 字符串 * 验证 OAuth2 环境变量是否齐全
* @param {string} jsonStr - JSON 字符串 * @returns {{ valid: boolean, issues: string[], diagnostics: object }}
* @returns {{ valid: boolean, credentials: object|null, issues: string[], diagnostics: object }}
*/ */
function validateServiceAccountJSON(jsonStr) { function validateOAuth2Credentials() {
const result = { const result = {
valid: false, valid: false,
credentials: null,
issues: [], issues: [],
diagnostics: { diagnostics: {
input_length: 0, auth_mode: 'oauth2',
is_empty: true, vars_checked: REQUIRED_OAUTH2_VARS.length,
json_parseable: false, vars_present: 0,
is_object: false, missing_vars: []
has_all_required_fields: false,
missing_fields: [],
field_checks: {},
truncation_suspected: false
} }
}; };
// ① 空值检查 for (const varName of REQUIRED_OAUTH2_VARS) {
if (!jsonStr || typeof jsonStr !== 'string') { const val = process.env[varName];
result.issues.push('Credential string is empty or not a string'); if (!val || val.trim().length === 0) {
return result; result.diagnostics.missing_vars.push(varName);
} result.issues.push(`Missing or empty environment variable: ${varName}`);
} else {
const trimmed = jsonStr.trim(); result.diagnostics.vars_present++;
result.diagnostics.input_length = trimmed.length;
result.diagnostics.is_empty = trimmed.length === 0;
if (trimmed.length === 0) {
result.issues.push('Credential string is empty after trimming');
return result;
}
// ② 基本结构检查(闭合括号)
if (!trimmed.startsWith('{')) {
result.issues.push(`JSON does not start with '{' (starts with '${trimmed[0]}')`);
}
if (!trimmed.endsWith('}')) {
result.issues.push(`JSON does not end with '}' (ends with '${trimmed[trimmed.length - 1]}')`);
result.diagnostics.truncation_suspected = true;
}
// 检查大括号平衡
let braceDepth = 0;
for (let i = 0; i < trimmed.length; i++) {
if (trimmed[i] === '{') braceDepth++;
else if (trimmed[i] === '}') braceDepth--;
}
if (braceDepth !== 0) {
result.issues.push(`Unbalanced braces: depth=${braceDepth} (${braceDepth > 0 ? 'missing closing }' : 'extra closing }'})`);
result.diagnostics.truncation_suspected = braceDepth > 0;
}
// ③ JSON 解析
let parsed;
try {
parsed = JSON.parse(trimmed);
result.diagnostics.json_parseable = true;
} catch (err) {
result.issues.push(`JSON parse error: ${err.message}`);
// 尝试诊断截断位置
const posMatch = err.message.match(/position (\d+)/);
if (posMatch) {
const pos = parseInt(posMatch[1], 10);
result.diagnostics.error_position = pos;
result.diagnostics.context_at_error = trimmed.substring(
Math.max(0, pos - 30),
Math.min(trimmed.length, pos + 30)
);
}
return result;
}
// ④ 类型检查
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
result.issues.push('Parsed JSON is not a plain object');
return result;
}
result.diagnostics.is_object = true;
result.credentials = parsed;
// ⑤ 必需字段检查
for (const field of REQUIRED_FIELDS) {
if (!(field in parsed) || parsed[field] === undefined || parsed[field] === null || parsed[field] === '') {
result.diagnostics.missing_fields.push(field);
result.issues.push(`Missing or empty required field: '${field}'`);
} }
} }
result.diagnostics.has_all_required_fields = result.diagnostics.missing_fields.length === 0;
// ⑥ 字段格式校验
if (parsed.type) {
const typeOk = parsed.type === 'service_account';
result.diagnostics.field_checks.type = typeOk ? 'ok' : `expected 'service_account', got '${parsed.type}'`;
if (!typeOk) result.issues.push(`Field 'type' should be 'service_account', got '${parsed.type}'`);
}
if (parsed.private_key) {
const pkStr = String(parsed.private_key);
const hasBegin = pkStr.includes('-----BEGIN');
const hasEnd = pkStr.includes('-----END');
result.diagnostics.field_checks.private_key_format = hasBegin && hasEnd ? 'ok' : 'PEM markers missing';
if (!hasBegin || !hasEnd) {
result.issues.push('Field \'private_key\' does not contain valid PEM BEGIN/END markers');
result.diagnostics.truncation_suspected = true;
}
}
if (parsed.client_email) {
const emailStr = String(parsed.client_email);
const emailOk = /^[^@]+@[^@]+\.iam\.gserviceaccount\.com$/.test(emailStr);
result.diagnostics.field_checks.client_email_format = emailOk ? 'ok' : 'not a valid service account email';
if (!emailOk) result.issues.push('Field \'client_email\' does not match service account email pattern');
}
// ⑦ 最终判定
result.valid = result.issues.length === 0; result.valid = result.issues.length === 0;
return result; return result;
} }
/** /**
* 生成人类可读的诊断报告 * 生成人类可读的诊断报告
* @param {object} validationResult - validateServiceAccountJSON 的返回值 * @param {object} validationResult - validateOAuth2Credentials 的返回值
* @returns {string} * @returns {string}
*/ */
function formatDiagnosticReport(validationResult) { function formatDiagnosticReport(validationResult) {
@ -167,31 +66,15 @@ function formatDiagnosticReport(validationResult) {
const lines = []; const lines = [];
lines.push('╔════════════════════════════════════════════════════════╗'); lines.push('╔════════════════════════════════════════════════════════╗');
lines.push('║ 🛰️ 天眼密钥流完整性校验报告 (Credential Audit) ║'); lines.push('║ 🛰️ 天眼 OAuth2 凭据校验报告 (Credential Audit) ║');
lines.push('╚════════════════════════════════════════════════════════╝'); lines.push('╚════════════════════════════════════════════════════════╝');
lines.push(''); lines.push('');
lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`); lines.push(`状态: ${r.valid ? '✅ 绿色信号 (PASS)' : '🔴 逻辑红区 (FAIL)'}`);
lines.push(`输入长度: ${r.diagnostics.input_length} bytes`); lines.push(`认证模式: OAuth2 代理人`);
lines.push(`JSON 可解析: ${r.diagnostics.json_parseable ? '是' : '否'}`); lines.push(`环境变量检查: ${r.diagnostics.vars_present}/${r.diagnostics.vars_checked} 齐全`);
lines.push(`是否为对象: ${r.diagnostics.is_object ? '是' : '否'}`);
lines.push(`全部必需字段: ${r.diagnostics.has_all_required_fields ? '齐全' : '缺失'}`);
lines.push(`截断嫌疑: ${r.diagnostics.truncation_suspected ? '⚠️ 是' : '否'}`);
if (r.diagnostics.missing_fields.length > 0) { if (r.diagnostics.missing_vars.length > 0) {
lines.push(`缺失字段: ${r.diagnostics.missing_fields.join(', ')}`); lines.push(`缺失变量: ${r.diagnostics.missing_vars.join(', ')}`);
}
if (r.diagnostics.error_position !== undefined) {
lines.push(`错误位置: position ${r.diagnostics.error_position}`);
lines.push(`上下文: ...${r.diagnostics.context_at_error}...`);
}
if (Object.keys(r.diagnostics.field_checks).length > 0) {
lines.push('');
lines.push('字段检查:');
for (const [k, v] of Object.entries(r.diagnostics.field_checks)) {
lines.push(` ${k}: ${v}`);
}
} }
if (r.issues.length > 0) { if (r.issues.length > 0) {
@ -209,26 +92,18 @@ function formatDiagnosticReport(validationResult) {
// 模块导出 // 模块导出
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
module.exports = { validateServiceAccountJSON, formatDiagnosticReport, REQUIRED_FIELDS }; module.exports = { validateOAuth2Credentials, formatDiagnosticReport, REQUIRED_OAUTH2_VARS };
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
// CLI 模式 // CLI 模式
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
if (require.main === module) { if (require.main === module) {
const jsonStr = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; const result = validateOAuth2Credentials();
if (!jsonStr) {
console.error('[credential-validator] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
process.exit(1);
}
const result = validateServiceAccountJSON(jsonStr);
console.log(formatDiagnosticReport(result)); console.log(formatDiagnosticReport(result));
console.log(''); console.log('');
console.log('---CREDENTIAL_AUDIT_JSON---'); console.log('---CREDENTIAL_AUDIT_JSON---');
// 不输出 credentials 本身以避免泄露密钥 console.log(JSON.stringify(result, null, 2));
const safeResult = { ...result, credentials: result.credentials ? '[REDACTED]' : null };
console.log(JSON.stringify(safeResult, null, 2));
process.exit(result.valid ? 0 : 1); process.exit(result.valid ? 0 : 1);
} }

View File

@ -11,7 +11,9 @@
* 完成后在 Issue 下回复文档/表格直连 ID * 完成后在 Issue 下回复文档/表格直连 ID
* *
* 环境变量 * 环境变量
* - GOOGLE_DRIVE_SERVICE_ACCOUNT: Service Account JSON 密钥 * - GDRIVE_CLIENT_ID: OAuth 客户端 ID
* - GDRIVE_CLIENT_SECRET: OAuth 客户端密钥
* - GDRIVE_REFRESH_TOKEN: 长效刷新令牌
* - DRIVE_LANDING_FOLDER_ID: 目标 Drive 文件夹 ID * - DRIVE_LANDING_FOLDER_ID: 目标 Drive 文件夹 ID
* - GITHUB_TOKEN: GitHub API 令牌 * - GITHUB_TOKEN: GitHub API 令牌
* - ISSUE_NUMBER: Issue 编号 * - ISSUE_NUMBER: Issue 编号
@ -36,35 +38,12 @@ const TAG_TABLE = '[TCS-TABLE]';
const LANDING_SUBFOLDER = 'tcs-semantic-landing'; const LANDING_SUBFOLDER = 'tcs-semantic-landing';
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
// Google API 认证 // Google API 认证OAuth2 代理人模式)
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
function buildAuth(serviceAccountJson) { function buildAuth() {
let credentials; const { getOAuth2Client } = require('./grid-db/drive-auth');
try { return getOAuth2Client();
const { validateServiceAccountJSON, formatDiagnosticReport } = require('./skyeye/credential-validator');
const validation = validateServiceAccountJSON(serviceAccountJson);
if (!validation.valid) {
console.error('[semantic-landing] 🔴 Credential validation failed:');
console.error(formatDiagnosticReport(validation));
throw new Error('Service account credential validation failed');
}
credentials = validation.credentials;
console.log('[semantic-landing] ✅ Service account credentials validated');
} catch (err) {
if (err.message === 'Service account credential validation failed') throw err;
// Fallback: validator module unavailable, try direct parse
console.log('[semantic-landing] Validator unavailable, using direct JSON.parse');
credentials = JSON.parse(serviceAccountJson);
}
return new google.auth.GoogleAuth({
credentials,
scopes: [
'https://www.googleapis.com/auth/drive',
'https://www.googleapis.com/auth/documents',
'https://www.googleapis.com/auth/spreadsheets'
]
});
} }
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
@ -465,7 +444,6 @@ function postIssueComment(token, repo, issueNumber, body) {
// ═══════════════════════════════════════════════ // ═══════════════════════════════════════════════
async function main() { async function main() {
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT;
const rootFolderId = process.env.DRIVE_LANDING_FOLDER_ID; const rootFolderId = process.env.DRIVE_LANDING_FOLDER_ID;
const githubToken = process.env.GITHUB_TOKEN; const githubToken = process.env.GITHUB_TOKEN;
const issueNumber = process.env.ISSUE_NUMBER; const issueNumber = process.env.ISSUE_NUMBER;
@ -474,10 +452,6 @@ async function main() {
const repo = process.env.GITHUB_REPOSITORY; const repo = process.env.GITHUB_REPOSITORY;
// 验证必需环境变量 // 验证必需环境变量
if (!serviceAccountJson) {
console.error('[semantic-landing] GOOGLE_DRIVE_SERVICE_ACCOUNT not set');
process.exit(1);
}
if (!rootFolderId) { if (!rootFolderId) {
console.error('[semantic-landing] DRIVE_LANDING_FOLDER_ID not set'); console.error('[semantic-landing] DRIVE_LANDING_FOLDER_ID not set');
process.exit(1); process.exit(1);
@ -499,9 +473,10 @@ async function main() {
console.log(`[semantic-landing] Processing Issue #${issueNumber}: ${issueTitle}`); console.log(`[semantic-landing] Processing Issue #${issueNumber}: ${issueTitle}`);
console.log(`[semantic-landing] Type: ${isMemo ? 'MEMO (Google Docs)' : 'TABLE (Google Sheets)'}`); console.log(`[semantic-landing] Type: ${isMemo ? 'MEMO (Google Docs)' : 'TABLE (Google Sheets)'}`);
// 初始化 Google API // 初始化 Google APIOAuth2 代理人模式)
const auth = buildAuth(serviceAccountJson); const auth = buildAuth();
const drive = google.drive({ version: 'v3', auth }); const drive = google.drive({ version: 'v3', auth });
console.log('[semantic-landing] ✅ OAuth2 credentials configured');
// 确保子文件夹存在 // 确保子文件夹存在
const landingFolderId = await getOrCreateFolder(drive, rootFolderId, LANDING_SUBFOLDER); const landingFolderId = await getOrCreateFolder(drive, rootFolderId, LANDING_SUBFOLDER);

View File

@ -199,27 +199,27 @@ function ensureDirectories() {
} }
/** /**
* 天眼密钥流校验 验证 GOOGLE_DRIVE_SERVICE_ACCOUNT 环境变量 * 天眼 OAuth2 凭据校验 验证 GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN
* 仅在环境变量可用时执行CI 环境 * 仅在环境变量可用时执行CI 环境
*/ */
function validateCredentials() { function validateCredentials() {
const result = { status: 'skipped', issues: [] }; const result = { status: 'skipped', issues: [] };
const serviceAccountJson = process.env.GOOGLE_DRIVE_SERVICE_ACCOUNT; const hasAnyVar = process.env.GDRIVE_CLIENT_ID || process.env.GDRIVE_CLIENT_SECRET || process.env.GDRIVE_REFRESH_TOKEN;
if (!serviceAccountJson) { if (!hasAnyVar) {
result.reason = 'GOOGLE_DRIVE_SERVICE_ACCOUNT not available'; result.reason = 'OAuth2 credentials not available (GDRIVE_CLIENT_ID / GDRIVE_CLIENT_SECRET / GDRIVE_REFRESH_TOKEN)';
return result; return result;
} }
try { try {
const { validateServiceAccountJSON } = require('../../scripts/skyeye/credential-validator'); const { validateOAuth2Credentials } = require('../../scripts/skyeye/credential-validator');
const validation = validateServiceAccountJSON(serviceAccountJson); const validation = validateOAuth2Credentials();
result.status = validation.valid ? 'pass' : 'fail'; result.status = validation.valid ? 'pass' : 'fail';
result.issues = validation.issues; result.issues = validation.issues;
if (!validation.valid) { if (!validation.valid) {
console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`); console.log(` [CREDENTIAL] 🔴 Validation failed: ${validation.issues.join('; ')}`);
} else { } else {
console.log(' [CREDENTIAL] ✅ Service account credentials valid'); console.log(' [CREDENTIAL] ✅ OAuth2 credentials valid');
} }
} catch (e) { } catch (e) {
result.status = 'error'; result.status = 'error';