feat: Z38 grant Yuheng agents ready-to-use access

Complete permissions during every bind and token path, expose effective access in the console, and keep the platform return path globally available.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-08-07 16:11:13 +08:00
parent 4f9087bd38
commit 43dc708f1e
11 changed files with 285 additions and 145 deletions

View File

@@ -9,16 +9,17 @@ import (
"aijianzhan/platform/internal/authx"
"aijianzhan/platform/internal/dbsync"
"aijianzhan/platform/internal/schema"
"aijianzhan/platform/internal/tenantperm"
"aijianzhan/platform/internal/types"
"aijianzhan/platform/internal/userstore"
"aijianzhan/platform/internal/yuhticket"
)
type YuhengTicketExchangeReq struct {
Ticket string `json:"ticket"`
HostKey string `json:"host_key"` // 须与票内一致
LocalDatabaseID string `json:"local_database_id,omitempty"`
Name string `json:"name,omitempty"`
Ticket string `json:"ticket"`
HostKey string `json:"host_key"` // 须与票内一致
LocalDatabaseID string `json:"local_database_id,omitempty"`
Name string `json:"name,omitempty"`
}
type YuhengTicketExchangeResp struct {
@@ -80,7 +81,7 @@ func (l *AuthLogic) ExchangeYuhengTicket(req YuhengTicketExchangeReq) (*YuhengTi
if localID == "" {
localID = strings.TrimSpace(claims.LocalDatabaseID)
}
acc, secret, err := l.bindUserHostSync(u, hostKey, name, localID, "yuheng-ticket")
acc, err := l.bindUserHostSync(u, hostKey, name, localID, "yuheng-ticket")
if err != nil {
return nil, err
}
@@ -90,14 +91,15 @@ func (l *AuthLogic) ExchangeYuhengTicket(req YuhengTicketExchangeReq) (*YuhengTi
acc = refreshed
}
}
if err := l.ensureAgentSyncPerm(acc); err != nil {
return nil, err
}
// 重新加载 perms
acc, err = l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID)
acc, err = l.ensureYuhengAgentPerms(acc)
if err != nil {
return nil, err
}
// 补权成功后才轮换密钥,禁止“绑定成功但业务接口 403”。
secret, err := l.svcCtx.Agents.RotateSecret(l.ctx, acc.TenantID, acc.AgentID)
if err != nil {
return nil, fmt.Errorf("rotate client_secret: %w", err)
}
token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms)
if err != nil {
return nil, err
@@ -130,38 +132,61 @@ func (l *AuthLogic) ExchangeYuhengTicket(req YuhengTicketExchangeReq) (*YuhengTi
OK: true,
ClientID: acc.ClientID,
ClientSecret: secret,
Message: "凭票换票成功(仅宇恒)",
Message: "绑定并授权完成(仅宇恒)",
}, nil
}
func (l *AuthLogic) ensureAgentSyncPerm(acc *agentstore.Account) error {
var yuhengAgentPerms = []string{
authx.Perm读取模块,
authx.Perm写入模块,
authx.Perm发布模块,
authx.Perm查询数据,
authx.Perm新增数据,
authx.Perm导入数据,
authx.Perm上传文件,
authx.Perm下载文件,
authx.Perm数据同步,
}
// ensureYuhengAgentPerms 以并集方式补齐建站权限包,保留账号已有权限。
func (l *AuthLogic) ensureYuhengAgentPerms(acc *agentstore.Account) (*agentstore.Account, error) {
if acc == nil {
return nil
}
has := false
for _, p := range acc.Perms {
if p == authx.Perm数据同步 {
has = true
break
}
}
if has && acc.Status == agentstore.StatusActive {
return nil
return nil, fmt.Errorf("agent account unavailable")
}
perms := append([]string{}, acc.Perms...)
if !has {
perms = append(perms, authx.Perm数据同步)
seen := make(map[string]struct{}, len(perms)+len(yuhengAgentPerms))
for _, p := range acc.Perms {
seen[authx.NormalizePerm(p)] = struct{}{}
}
changed := acc.Status != agentstore.StatusActive
for _, p := range yuhengAgentPerms {
if _, ok := seen[p]; ok {
continue
}
perms = append(perms, p)
seen[p] = struct{}{}
changed = true
}
if !changed {
return acc, nil
}
perms = authx.NormalizePerms(perms)
if err := tenantperm.MustAllow(l.ctx, l.svcCtx.TenantPerm, acc.TenantID, perms); err != nil {
return nil, fmt.Errorf("绑定授权超出公司权限额度: %w", err)
}
st := agentstore.StatusActive
_, err := l.svcCtx.Agents.Update(l.ctx, acc.TenantID, acc.AgentID, agentstore.UpdateInput{
updated, err := l.svcCtx.Agents.Update(l.ctx, acc.TenantID, acc.AgentID, agentstore.UpdateInput{
Status: &st,
Perms: &perms,
})
return err
if err != nil {
return nil, fmt.Errorf("绑定授权失败: %w", err)
}
return updated, nil
}
// bindUserHostSync 将 host_key 智能体挂到用户公司默认同步落点并写 Binding。
func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID, note string) (*agentstore.Account, string, error) {
func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID, note string) (*agentstore.Account, error) {
cfg := l.svcCtx.Config.DBSync
driver := dbsync.Driver(strings.TrimSpace(cfg.DefaultRemoteDriver))
if driver == "" {
@@ -173,9 +198,8 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
RemoteDSN: strings.TrimSpace(cfg.DefaultRemoteDSN),
})
if err != nil {
return nil, "", fmt.Errorf("ensure channel: %w", err)
return nil, fmt.Errorf("ensure channel: %w", err)
}
var secret string
acc, err := l.svcCtx.Agents.FindByHostKey(l.ctx, hostKey)
if err != nil {
if name == "" {
@@ -183,13 +207,14 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
}
created, sec, _, regErr := l.svcCtx.Agents.Register(l.ctx, u.TenantID, name, hostKey)
if regErr != nil {
return nil, "", regErr
return nil, regErr
}
acc, secret = created, sec
acc = created
_ = sec
} else if acc.TenantID != u.TenantID && acc.Status == agentstore.StatusPending {
// 允许 pending 迁公司;已激活异租户拒绝
} else if acc.TenantID != u.TenantID {
return nil, "", fmt.Errorf("host_key 已绑定其它公司")
return nil, fmt.Errorf("host_key 已绑定其它公司")
}
online := fmt.Sprintf("%s_u%d", dbsync.ResolveOnlineDBID("", ch.ID), u.UserID)
// 中文昵称可接受:写入 Binding 展示字段;物理库名映射为 user_{id} / 哈希Z35
@@ -200,12 +225,7 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
dbName := schema.SafeSyncDatabaseName(display, acc.AgentID, u.UserID)
updated, err := l.svcCtx.Agents.AttachSyncBind(l.ctx, acc.AgentID, u.TenantID, ch.ID, online, dbName, true)
if err != nil {
return nil, "", err
}
// Z28绑定成功后轮换密钥并回传宇恒可直接换票无需再 SelfRegister pending
secret, err = l.svcCtx.Agents.RotateSecret(l.ctx, updated.TenantID, updated.AgentID)
if err != nil {
return nil, "", fmt.Errorf("rotate client_secret: %w", err)
return nil, err
}
if localID == "" {
localID = "host:" + hostKey
@@ -220,5 +240,5 @@ func (l *AuthLogic) bindUserHostSync(u *userstore.User, hostKey, name, localID,
DisplayName: display,
Note: note,
})
return updated, secret, nil
return updated, nil
}