Guide Feishu app setup and surface tenant lookup codes

This commit is contained in:
opsx committed 2026-10-22 19:09:57 +08:00
1 parent eb3861bbe8
commit 7e50443bba
6 files changed
+76 -5

No files matched your search

+18 -4
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
@@ -19,6 +20,14 @@ import (
const feishuTenantToken = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
const feishuTenantQuery = "https://open.feishu.cn/open-apis/tenant/v2/tenant/query"
// Do not include upstream msg: it is untrusted and may contain request data.
func feishuFailure(stage string, status int, code *int) error {
if code != nil {
return fmt.Errorf("%s失败(飞书错误码 %d,HTTP %d);请按错误码检查应用权限、发布状态及接口配置", stage, *code, status)
}
return fmt.Errorf("%s失败(HTTP %d,未获得飞书错误码);请检查飞书接口配置", stage, status)
}
func secureAdmin(c *gin.Context) bool {
if c.Request.TLS != nil {
return true
@@ -68,8 +77,12 @@ func queryFeishuTenant(ctx context.Context, creds tenantCredentials) (string, st
Code *int `json:"code"`
TenantAccessToken string `json:"tenant_access_token"`
}
if resp.StatusCode != http.StatusOK || json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&token) != nil || token.Code == nil || *token.Code != 0 || token.TenantAccessToken == "" {
return "", "", errors.New("获取飞书访问凭据失败,请检查 App ID 和 App Secret")
decodeErr := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&token)
if resp.StatusCode != http.StatusOK || decodeErr != nil || token.Code == nil || *token.Code != 0 {
return "", "", feishuFailure("获取飞书访问凭据", resp.StatusCode, token.Code)
}
if token.TenantAccessToken == "" {
return "", "", errors.New("飞书未返回 tenant_access_token")
}
req, err = http.NewRequestWithContext(ctx, http.MethodGet, feishuTenantQuery, nil)
if err != nil {
@@ -90,8 +103,9 @@ func queryFeishuTenant(ctx context.Context, creds tenantCredentials) (string, st
} `json:"tenant"`
} `json:"data"`
}
if resp.StatusCode != http.StatusOK || json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result) != nil || result.Code == nil || *result.Code != 0 {
return "", "", errors.New("查询飞书企业信息失败,请确认应用已开通企业信息读取权限")
decodeErr = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result)
if resp.StatusCode != http.StatusOK || decodeErr != nil || result.Code == nil || *result.Code != 0 {
return "", "", feishuFailure("查询飞书企业信息", resp.StatusCode, result.Code)
}
if !validTenant(result.Data.Tenant.TenantKey) {
return "", "", errors.New("飞书未返回有效的企业标识")
@@ -2,6 +2,7 @@ package identityprovider
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
@@ -123,6 +124,34 @@ func TestQueryTenantRejectsWrongAppAndUpstreamFailure(t *testing.T) {
}
}
func TestTenantErrorsIdentifyStageWithoutLeakingFeishuMessage(t *testing.T) {
previous := upstreamClient
defer func() { upstreamClient = previous }()
for _, tc := range []struct {
name, endpoint, payload, want string
status int
}{
{"tenant denied", feishuTenantQuery, `{"code":99991672,"msg":"secret in upstream error"}`, "99991672", 403},
{"unexpected tenant endpoint", feishuTenantQuery, `not json`, "HTTP 404", 404},
{"token denied", feishuTenantToken, `{"code":10003,"msg":"secret in upstream error"}`, "10003", 401},
} {
t.Run(tc.name, func(t *testing.T) {
upstreamClient = &http.Client{Transport: transportFunc(func(req *http.Request) (*http.Response, error) {
body := `{"code":0,"tenant_access_token":"temporary-token"}`
status := 200
if req.URL.String() == tc.endpoint {
body, status = tc.payload, tc.status
}
return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header)}, nil
})}
_, _, err := queryFeishuTenant(context.Background(), tenantCredentials{AppID: "cli_test", AppSecret: "private-value"})
if err == nil || !strings.Contains(err.Error(), tc.want) || strings.Contains(err.Error(), "secret in upstream error") || strings.Contains(err.Error(), "private-value") {
t.Fatalf("unsafe or unhelpful error: %v", err)
}
})
}
}
func TestAdminNeverEchoesFeishuSecretAndKeepsEncryptedValue(t *testing.T) {
dir := t.TempDir()
t.Setenv("VPN_DATA_DIR", dir)