0.11.2: 旗舰版第三轮完善(通用聊天/企微钉钉飞书扫码登录/个人安全策略)
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时 API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计; 会话哈希链完整性 + busy 租约防并发,失败不落库。 - 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置 (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示; one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点 固定公网 URL 复用 public-only 拨号。 - 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/ 扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信 (新增 security 类别),会话索引只存令牌摘要并惰性清理。 - 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失; 全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
This commit is contained in:
@@ -72,6 +72,85 @@ func (s *RuntimeCredentials) Ensure(ctx context.Context, applicationID string, d
|
||||
return secret, record.ID, nil
|
||||
}
|
||||
|
||||
// EnsureUser returns the portal user's personal runtime credential used by the
|
||||
// general chat. It is created lazily on first use with limits derived from the
|
||||
// user's approved model requests, and bound to the user's department tenant so
|
||||
// usage/audit are attributed to the user's own key. Idempotent: the unique
|
||||
// primary key makes concurrent first-use requests converge on one credential.
|
||||
func (s *RuntimeCredentials) EnsureUser(ctx context.Context, userID string, departmentID *string, rpm int, monthlyTokens int64) (string, string, error) {
|
||||
if s == nil || s.pool == nil || s.repository == nil || s.cipher == nil {
|
||||
return "", "", errors.New("runtime credentials unavailable")
|
||||
}
|
||||
var encrypted []byte
|
||||
var version int
|
||||
var keyID string
|
||||
err := s.pool.QueryRow(ctx, `SELECT encrypted_key,key_kek_version,api_key_id::text FROM gateway.portal_user_runtime_credentials WHERE portal_user_id=$1`, userID).Scan(&encrypted, &version, &keyID)
|
||||
if err == nil {
|
||||
plain, decryptErr := s.cipher.Decrypt(encrypted, version)
|
||||
return string(plain), keyID, decryptErr
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", "", err
|
||||
}
|
||||
if rpm < 1 {
|
||||
rpm = 120
|
||||
}
|
||||
record, secret, err := s.repository.Create(ctx, "portal-chat-runtime", []string{"gateway:invoke"}, rpm, 0, monthlyTokens, nil, "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
encrypted, version, err = s.cipher.Encrypt([]byte(secret))
|
||||
if err != nil {
|
||||
_, _ = s.repository.Revoke(ctx, record.ID, "")
|
||||
return "", "", err
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
_, _ = s.repository.Revoke(ctx, record.ID, "")
|
||||
return "", "", err
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
_, err = tx.Exec(ctx, `UPDATE gateway.api_keys SET portal_user_id=$2,tenant_id=$3 WHERE id=$1`, record.ID, userID, departmentID)
|
||||
if err == nil {
|
||||
_, err = tx.Exec(ctx, `INSERT INTO gateway.portal_user_runtime_credentials(portal_user_id,api_key_id,encrypted_key,key_kek_version) VALUES($1,$2,$3,$4)`, userID, record.ID, encrypted, version)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback(ctx)
|
||||
_, _ = s.repository.Revoke(ctx, record.ID, "")
|
||||
// A concurrent first-use request may have won the unique-key race.
|
||||
var pgError *pgconn.PgError
|
||||
if errors.As(err, &pgError) && pgError.Code == "23505" {
|
||||
return s.EnsureUser(ctx, userID, departmentID, rpm, monthlyTokens)
|
||||
}
|
||||
return "", "", fmt.Errorf("store portal runtime credential: %w", err)
|
||||
}
|
||||
if err = tx.Commit(ctx); err != nil {
|
||||
_, _ = s.repository.Revoke(ctx, record.ID, "")
|
||||
return "", "", err
|
||||
}
|
||||
return secret, record.ID, nil
|
||||
}
|
||||
|
||||
// UserSecret returns the portal user's runtime credential plaintext for the
|
||||
// duration of the request. Empty when not yet provisioned.
|
||||
func (s *RuntimeCredentials) UserSecret(ctx context.Context, userID string) (string, string, error) {
|
||||
if s == nil || s.pool == nil || s.cipher == nil {
|
||||
return "", "", errors.New("runtime credentials unavailable")
|
||||
}
|
||||
var encrypted []byte
|
||||
var version int
|
||||
var keyID string
|
||||
err := s.pool.QueryRow(ctx, `SELECT encrypted_key,key_kek_version,api_key_id::text FROM gateway.portal_user_runtime_credentials WHERE portal_user_id=$1`, userID).Scan(&encrypted, &version, &keyID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
plain, err := s.cipher.Decrypt(encrypted, version)
|
||||
return string(plain), keyID, err
|
||||
}
|
||||
|
||||
func (s *RuntimeCredentials) Metadata(ctx context.Context, applicationID string) ([]map[string]any, error) {
|
||||
rows, err := s.pool.Query(ctx, `SELECT c.api_key_id::text,k.key_prefix,c.department_id::text,k.enabled,c.created_at FROM gateway.application_runtime_credentials c JOIN gateway.api_keys k ON k.id=c.api_key_id WHERE c.application_id=$1 ORDER BY c.created_at`, applicationID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user