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:
@@ -255,6 +255,15 @@ func (l *AuthLogic) IssueClientCredentials(clientID, clientSecret string) (*type
|
||||
}
|
||||
}
|
||||
}
|
||||
// Z38:宇恒已绑定账号用 client_credentials 换票时自动修复存量缺权。
|
||||
if strings.TrimSpace(acc.HostKey) != "" &&
|
||||
strings.TrimSpace(acc.ChannelID) != "" &&
|
||||
strings.TrimSpace(acc.OnlineDBID) != "" {
|
||||
acc, err = l.ensureYuhengAgentPerms(acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
token, exp, err := authx.IssueAgentToken(l.svcCtx.JWT, acc.TenantID, acc.AgentID, acc.Perms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -128,16 +128,17 @@ type BindCodeRedeemReq struct {
|
||||
}
|
||||
|
||||
type BindCodeRedeemResp struct {
|
||||
OK bool `json:"ok"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
AgentID int64 `json:"agent_id"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"` // Z28:绑定成功回传,便于直接换票
|
||||
ChannelID string `json:"channel_id"`
|
||||
OnlineDBID string `json:"online_db_id"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
SyncBound bool `json:"sync_bound"`
|
||||
Message string `json:"message,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
AgentID int64 `json:"agent_id"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"` // Z28:绑定成功回传,便于直接换票
|
||||
ChannelID string `json:"channel_id"`
|
||||
OnlineDBID string `json:"online_db_id"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
SyncBound bool `json:"sync_bound"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp, error) {
|
||||
@@ -175,6 +176,10 @@ func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updated, err = l.ensureYuhengAgentPerms(updated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Z28:回传可换票凭证
|
||||
secret, err := l.svcCtx.Agents.RotateSecret(l.ctx, updated.TenantID, updated.AgentID)
|
||||
if err != nil {
|
||||
@@ -193,7 +198,8 @@ func (l *AuthLogic) RedeemBindCode(req BindCodeRedeemReq) (*BindCodeRedeemResp,
|
||||
OnlineDBID: updated.OnlineDBID,
|
||||
DatabaseName: updated.DatabaseName,
|
||||
SyncBound: true,
|
||||
Message: "绑定成功;请保存 client_id/client_secret 用于换票",
|
||||
Permissions: append([]string{}, updated.Perms...),
|
||||
Message: "绑定并授权完成;请保存 client_id/client_secret 用于换票",
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -320,16 +326,17 @@ type PhoneConfirmReq struct {
|
||||
}
|
||||
|
||||
type PhoneConfirmResp struct {
|
||||
OK bool `json:"ok"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
AgentID int64 `json:"agent_id"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"` // Z28:绑定成功回传
|
||||
ChannelID string `json:"channel_id"`
|
||||
OnlineDBID string `json:"online_db_id"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
SyncBound bool `json:"sync_bound"`
|
||||
Message string `json:"message,omitempty"`
|
||||
OK bool `json:"ok"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
AgentID int64 `json:"agent_id"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
ClientSecret string `json:"client_secret,omitempty"` // Z28:绑定成功回传
|
||||
ChannelID string `json:"channel_id"`
|
||||
OnlineDBID string `json:"online_db_id"`
|
||||
DatabaseName string `json:"database_name,omitempty"`
|
||||
SyncBound bool `json:"sync_bound"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (l *AuthLogic) requirePhoneBindProof(phone string, attested bool, smsCode string) error {
|
||||
@@ -386,11 +393,18 @@ func (l *AuthLogic) PhoneConfirm(req PhoneConfirmReq) (*PhoneConfirmResp, error)
|
||||
if u.TenantID <= 0 {
|
||||
return nil, fmt.Errorf("该账号尚未加入公司")
|
||||
}
|
||||
updated, secret, err := l.bindUserHostSync(u, hostKey, req.Name, req.LocalDBID, "phone-confirm")
|
||||
updated, err := l.bindUserHostSync(u, hostKey, req.Name, req.LocalDBID, "phone-confirm")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = l.ensureAgentSyncPerm(updated)
|
||||
updated, err = l.ensureYuhengAgentPerms(updated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret, err := l.svcCtx.Agents.RotateSecret(l.ctx, updated.TenantID, updated.AgentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rotate client_secret: %w", err)
|
||||
}
|
||||
_ = l.writeBindAudit("phone_confirm_bind", u.TenantID, updated.AgentID, map[string]any{
|
||||
"phone": phone, "user_id": u.UserID, "channel_id": updated.ChannelID, "online_db_id": updated.OnlineDBID,
|
||||
})
|
||||
@@ -404,7 +418,8 @@ func (l *AuthLogic) PhoneConfirm(req PhoneConfirmReq) (*PhoneConfirmResp, error)
|
||||
OnlineDBID: updated.OnlineDBID,
|
||||
DatabaseName: updated.DatabaseName,
|
||||
SyncBound: true,
|
||||
Message: "绑定成功;请保存 client_id/client_secret 用于换票",
|
||||
Permissions: append([]string{}, updated.Perms...),
|
||||
Message: "绑定并授权完成;请保存 client_id/client_secret 用于换票",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -124,10 +124,7 @@ func (l *AuthLogic) RestoreByYuhengHost(req YuhengRestoreByHostReq) (*YuhengRest
|
||||
return nil, restoreNeedBind("host_key 尚未完成同步绑定;请先绑定码/手机号绑定")
|
||||
}
|
||||
|
||||
if err := l.ensureAgentSyncPerm(acc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
acc, err = l.svcCtx.Agents.Get(l.ctx, acc.TenantID, acc.AgentID)
|
||||
acc, err = l.ensureYuhengAgentPerms(acc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -182,7 +179,7 @@ func (l *AuthLogic) RestoreByYuhengHost(req YuhengRestoreByHostReq) (*YuhengRest
|
||||
AgentID: acc.AgentID,
|
||||
HostKey: hostKey,
|
||||
PhoneMasked: phoneMasked,
|
||||
Message: "已按宇恒账号恢复同步绑定",
|
||||
Message: "已按宇恒账号恢复绑定并授权完成",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user