按桌面客户端统一方案 v0.1 + agent_management Sub Mode Runtime 对接,强制全量统一,不留兼容。
命名统一(强制,无兼容):
- 全仓 agnet/Agnet/AGNET → agent/Agent/AGENT:后端 Go(路由 /api/agent/*、env AGENT_*、
结构体/函数、19 个文件改名)、前端(agent-console/agent-hub、/api/agent 调用、i18n)、
DB(表 agent_*、列 agent_id)、compose/.env、文档、脚本。
- DB 加幂等迁移 renameAgnetTablesToAgent():启动时 rename 老 agnet_* 表/列,保住生产数据。
统一方案核心(10 项):
- callback 统一 /api/agent/callbacks/runtime-events(路由/广播URL/函数名)。
- artifact 兜底判定改用 Runtime 权威信号 metadata.synthesized(§7.2)+ 结构化 artifact_type。
- Manager→Runtime 路径对齐 /api/agent/sub-agile/deployments(§2.2),{deployment_id} 回退 swarm_id。
- 状态裁决 display_status:Manager 唯一裁判,completed 无有效产物→needs_codegen/
completed_without_deliverable(§10.6),接入 detail/timeline/workflow。
- GET /api/heicode/capabilities 能力发现(§6)。
- 模型策略 per_role(role_models)+ 收集 allowed_model_ids(§9)。
- resource_binding_id→secret_ref 服务端解析,客户端不再 inline secret_ref(§17.6)。
- 客户端统一路由层 /api/heicode/sub-agile|swarm/*(task≡deployment,复用控制面)+ workflow 投影。
- 日志分层 user_logs/debug_logs(§13)。
验证:go build ./... + go test(controller/router/model/middleware)全绿;前端 tsc -b + rsbuild build 通过。
待部署:VM .env 的 AGNET_*→AGENT_*;启动迁移自动 rename 表;其他三仓库需同步切到 /api/agent。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
561 lines
19 KiB
Go
561 lines
19 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/heicode/manager/common"
|
|
"github.com/heicode/manager/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
agentApprovalStatusPending = "pending"
|
|
agentApprovalStatusApproved = "approved"
|
|
agentApprovalStatusRejected = "rejected"
|
|
agentApprovalStatusExpired = "expired"
|
|
|
|
agentLeaseStatusActive = "active"
|
|
agentLeaseStatusExpired = "expired"
|
|
agentLeaseStatusRevoked = "revoked"
|
|
|
|
defaultAgentApprovalTTLSeconds = 15 * 60
|
|
maxAgentApprovalTTLSeconds = 60 * 60
|
|
)
|
|
|
|
type agentApprovalPayload struct {
|
|
DeploymentID string `json:"deployment_id"`
|
|
BindingScope string `json:"binding_scope"`
|
|
Operation string `json:"operation"`
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
ResourceScope string `json:"resource_scope"`
|
|
TargetRole string `json:"target_role"`
|
|
RiskLevel string `json:"risk_level"`
|
|
RequiresCredential bool `json:"requires_credential"`
|
|
SecretRef string `json:"secret_ref"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type agentDecisionPayload struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
type agentApprovalResponse struct {
|
|
ApprovalID string `json:"approval_id"`
|
|
UserId int `json:"user_id"`
|
|
DeploymentID string `json:"deployment_id"`
|
|
BindingScope string `json:"binding_scope"`
|
|
Operation string `json:"operation"`
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
ResourceScope string `json:"resource_scope"`
|
|
TargetRole string `json:"target_role"`
|
|
RiskLevel string `json:"risk_level"`
|
|
RequiresCredential bool `json:"requires_credential"`
|
|
CredentialLeaseID string `json:"credential_lease_id,omitempty"`
|
|
Status string `json:"status"`
|
|
RequestedBy string `json:"requested_by"`
|
|
DecidedBy string `json:"decided_by,omitempty"`
|
|
RequestReason string `json:"request_reason,omitempty"`
|
|
DecisionReason string `json:"decision_reason,omitempty"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
DecidedAt int64 `json:"decided_at,omitempty"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
CredentialLease *agentCredentialLeaseResponse `json:"credential_lease,omitempty"`
|
|
}
|
|
|
|
type agentCredentialLeaseResponse struct {
|
|
LeaseID string `json:"lease_id"`
|
|
CredentialRef string `json:"credential_ref"`
|
|
ApprovalID string `json:"approval_id"`
|
|
UserId int `json:"user_id"`
|
|
DeploymentID string `json:"deployment_id"`
|
|
BindingScope string `json:"binding_scope"`
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceType string `json:"resource_type"`
|
|
ResourceScope string `json:"resource_scope"`
|
|
TargetRole string `json:"target_role"`
|
|
Status string `json:"status"`
|
|
TTLSeconds int `json:"ttl_seconds"`
|
|
ExpiresAt int64 `json:"expires_at"`
|
|
RevokedAt int64 `json:"revoked_at,omitempty"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
func CreateAgentApprovalRequest(c *gin.Context) {
|
|
var payload agentApprovalPayload
|
|
if err := common.DecodeJson(c.Request.Body, &payload); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
payload, err := normalizeAgentApprovalPayload(payload)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
userID := c.GetInt("id")
|
|
if userID <= 0 {
|
|
common.ApiErrorMsg(c, "user authentication required")
|
|
return
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
approval := model.AgentApprovalRequest{
|
|
ApprovalID: "appr_" + common.GetUUID(),
|
|
UserId: userID,
|
|
DeploymentID: payload.DeploymentID,
|
|
BindingScope: payload.BindingScope,
|
|
Operation: payload.Operation,
|
|
ResourceID: payload.ResourceID,
|
|
ResourceType: payload.ResourceType,
|
|
ResourceScope: payload.ResourceScope,
|
|
TargetRole: payload.TargetRole,
|
|
RiskLevel: payload.RiskLevel,
|
|
RequiresCredential: payload.RequiresCredential,
|
|
SecretRef: payload.SecretRef,
|
|
Status: agentApprovalStatusPending,
|
|
RequestedBy: agentActorForUser(userID),
|
|
RequestReason: payload.Reason,
|
|
TTLSeconds: payload.TTLSeconds,
|
|
ExpiresAt: now + int64(payload.TTLSeconds)*1000,
|
|
}
|
|
if err := model.DB.Create(&approval).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
recordAgentApprovalAudit("approval.requested", &approval, nil, "ok", "")
|
|
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
|
|
}
|
|
|
|
func ListAgentApprovalRequests(c *gin.Context) {
|
|
userID := c.GetInt("id")
|
|
if userID <= 0 {
|
|
common.ApiErrorMsg(c, "user authentication required")
|
|
return
|
|
}
|
|
q := model.DB.Where("user_id = ?", userID)
|
|
statusFilter := strings.TrimSpace(c.Query("status"))
|
|
if statusFilter != "" {
|
|
q = q.Where("status = ?", statusFilter)
|
|
}
|
|
if deploymentID := strings.TrimSpace(c.Query("deployment_id")); deploymentID != "" {
|
|
q = q.Where("deployment_id = ?", deploymentID)
|
|
}
|
|
|
|
var approvals []model.AgentApprovalRequest
|
|
if err := q.Order("created_at desc, id desc").Limit(200).Find(&approvals).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
for i := range approvals {
|
|
expireAgentApprovalIfNeeded(&approvals[i])
|
|
}
|
|
|
|
items := make([]agentApprovalResponse, 0, len(approvals))
|
|
for _, approval := range approvals {
|
|
if statusFilter != "" && approval.Status != statusFilter {
|
|
continue
|
|
}
|
|
items = append(items, agentApprovalToResponse(approval, nil))
|
|
}
|
|
common.ApiSuccess(c, gin.H{"items": items})
|
|
}
|
|
|
|
func GetAgentApprovalRequest(c *gin.Context) {
|
|
approval, ok := findAgentApprovalForUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
expireAgentApprovalIfNeeded(&approval)
|
|
lease := findAgentCredentialLeaseByApproval(approval.ApprovalID)
|
|
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
|
|
}
|
|
|
|
func ApproveAgentApprovalRequest(c *gin.Context) {
|
|
approval, ok := findAgentApprovalForUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if expireAgentApprovalIfNeeded(&approval) {
|
|
common.ApiErrorMsg(c, "approval request expired")
|
|
return
|
|
}
|
|
if approval.Status != agentApprovalStatusPending {
|
|
common.ApiErrorMsg(c, "approval request is not pending")
|
|
return
|
|
}
|
|
|
|
var payload agentDecisionPayload
|
|
_ = common.DecodeJson(c.Request.Body, &payload)
|
|
now := time.Now().UnixMilli()
|
|
approval.Status = agentApprovalStatusApproved
|
|
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
|
|
approval.DecisionReason = strings.TrimSpace(payload.Reason)
|
|
approval.DecidedAt = now
|
|
|
|
var lease *model.AgentCredentialLease
|
|
err := model.DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Save(&approval).Error; err != nil {
|
|
return err
|
|
}
|
|
if !approval.RequiresCredential {
|
|
return nil
|
|
}
|
|
createdLease := model.AgentCredentialLease{
|
|
LeaseID: "lease_" + common.GetUUID(),
|
|
ApprovalID: approval.ApprovalID,
|
|
UserId: approval.UserId,
|
|
DeploymentID: approval.DeploymentID,
|
|
BindingScope: approval.BindingScope,
|
|
ResourceID: approval.ResourceID,
|
|
ResourceType: approval.ResourceType,
|
|
ResourceScope: approval.ResourceScope,
|
|
TargetRole: approval.TargetRole,
|
|
SecretRef: approval.SecretRef,
|
|
Status: agentLeaseStatusActive,
|
|
TTLSeconds: approval.TTLSeconds,
|
|
ExpiresAt: now + int64(approval.TTLSeconds)*1000,
|
|
}
|
|
createdLease.CredentialRef = "lease://agent/" + createdLease.LeaseID
|
|
if err := tx.Create(&createdLease).Error; err != nil {
|
|
return err
|
|
}
|
|
approval.CredentialLeaseID = createdLease.LeaseID
|
|
if err := tx.Save(&approval).Error; err != nil {
|
|
return err
|
|
}
|
|
lease = &createdLease
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
recordAgentApprovalAudit("approval.approved", &approval, nil, "ok", "")
|
|
if lease != nil {
|
|
recordAgentApprovalAudit("credential_lease.created", &approval, lease, "ok", "")
|
|
}
|
|
syncAgentRuntimeApprovalDecision(c, &approval, lease, agentApprovalStatusApproved)
|
|
common.ApiSuccess(c, agentApprovalToResponse(approval, lease))
|
|
}
|
|
|
|
func RejectAgentApprovalRequest(c *gin.Context) {
|
|
approval, ok := findAgentApprovalForUser(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if expireAgentApprovalIfNeeded(&approval) {
|
|
common.ApiErrorMsg(c, "approval request expired")
|
|
return
|
|
}
|
|
if approval.Status != agentApprovalStatusPending {
|
|
common.ApiErrorMsg(c, "approval request is not pending")
|
|
return
|
|
}
|
|
|
|
var payload agentDecisionPayload
|
|
_ = common.DecodeJson(c.Request.Body, &payload)
|
|
approval.Status = agentApprovalStatusRejected
|
|
approval.DecidedBy = agentActorForUser(c.GetInt("id"))
|
|
approval.DecisionReason = strings.TrimSpace(payload.Reason)
|
|
approval.DecidedAt = time.Now().UnixMilli()
|
|
if err := model.DB.Save(&approval).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
recordAgentApprovalAudit("approval.rejected", &approval, nil, "ok", "")
|
|
syncAgentRuntimeApprovalDecision(c, &approval, nil, agentApprovalStatusRejected)
|
|
common.ApiSuccess(c, agentApprovalToResponse(approval, nil))
|
|
}
|
|
|
|
func ListAgentCredentialLeases(c *gin.Context) {
|
|
userID := c.GetInt("id")
|
|
if userID <= 0 {
|
|
common.ApiErrorMsg(c, "user authentication required")
|
|
return
|
|
}
|
|
q := model.DB.Where("user_id = ?", userID)
|
|
statusFilter := strings.TrimSpace(c.Query("status"))
|
|
if statusFilter != "" {
|
|
q = q.Where("status = ?", statusFilter)
|
|
}
|
|
if deploymentID := strings.TrimSpace(c.Query("deployment_id")); deploymentID != "" {
|
|
q = q.Where("deployment_id = ?", deploymentID)
|
|
}
|
|
|
|
var leases []model.AgentCredentialLease
|
|
if err := q.Order("created_at desc, id desc").Limit(200).Find(&leases).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
items := make([]agentCredentialLeaseResponse, 0, len(leases))
|
|
for i := range leases {
|
|
expireAgentCredentialLeaseIfNeeded(&leases[i])
|
|
if statusFilter != "" && leases[i].Status != statusFilter {
|
|
continue
|
|
}
|
|
items = append(items, agentLeaseToResponse(leases[i]))
|
|
}
|
|
common.ApiSuccess(c, gin.H{"items": items})
|
|
}
|
|
|
|
func RevokeAgentCredentialLease(c *gin.Context) {
|
|
userID := c.GetInt("id")
|
|
if userID <= 0 {
|
|
common.ApiErrorMsg(c, "user authentication required")
|
|
return
|
|
}
|
|
leaseID := strings.TrimSpace(c.Param("lease_id"))
|
|
if leaseID == "" {
|
|
common.ApiErrorMsg(c, "lease_id required")
|
|
return
|
|
}
|
|
var lease model.AgentCredentialLease
|
|
if err := model.DB.Where("lease_id = ? AND user_id = ?", leaseID, userID).First(&lease).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
common.ApiErrorMsg(c, "credential lease not found")
|
|
return
|
|
}
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if expireAgentCredentialLeaseIfNeeded(&lease) {
|
|
common.ApiSuccess(c, agentLeaseToResponse(lease))
|
|
return
|
|
}
|
|
if lease.Status != agentLeaseStatusActive {
|
|
common.ApiErrorMsg(c, "credential lease is not active")
|
|
return
|
|
}
|
|
lease.Status = agentLeaseStatusRevoked
|
|
lease.RevokedAt = time.Now().UnixMilli()
|
|
if err := model.DB.Save(&lease).Error; err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
var approval model.AgentApprovalRequest
|
|
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
|
|
recordAgentApprovalAudit("credential_lease.revoked", &approval, &lease, "ok", "")
|
|
}
|
|
common.ApiSuccess(c, agentLeaseToResponse(lease))
|
|
}
|
|
|
|
func normalizeAgentApprovalPayload(p agentApprovalPayload) (agentApprovalPayload, error) {
|
|
p.DeploymentID = strings.TrimSpace(p.DeploymentID)
|
|
p.BindingScope = strings.TrimSpace(p.BindingScope)
|
|
p.Operation = strings.TrimSpace(p.Operation)
|
|
p.ResourceID = strings.TrimSpace(p.ResourceID)
|
|
p.ResourceType = strings.ToLower(strings.TrimSpace(p.ResourceType))
|
|
p.ResourceScope = strings.TrimSpace(p.ResourceScope)
|
|
p.TargetRole = strings.TrimSpace(p.TargetRole)
|
|
p.RiskLevel = strings.ToLower(strings.TrimSpace(p.RiskLevel))
|
|
p.SecretRef = strings.TrimSpace(p.SecretRef)
|
|
p.Reason = strings.TrimSpace(p.Reason)
|
|
|
|
if p.Operation == "" {
|
|
return p, errors.New("operation required")
|
|
}
|
|
if p.ResourceID == "" {
|
|
return p, errors.New("resource_id required")
|
|
}
|
|
if p.ResourceType == "" {
|
|
return p, errors.New("resource_type required")
|
|
}
|
|
if p.TargetRole == "" {
|
|
return p, errors.New("target_role required")
|
|
}
|
|
if p.RiskLevel == "" {
|
|
p.RiskLevel = "high"
|
|
}
|
|
if p.RiskLevel != "low" && p.RiskLevel != "medium" && p.RiskLevel != "high" && p.RiskLevel != "critical" {
|
|
return p, errors.New("risk_level must be low, medium, high, or critical")
|
|
}
|
|
if p.TTLSeconds <= 0 {
|
|
p.TTLSeconds = defaultAgentApprovalTTLSeconds
|
|
}
|
|
if p.TTLSeconds > maxAgentApprovalTTLSeconds {
|
|
p.TTLSeconds = maxAgentApprovalTTLSeconds
|
|
}
|
|
if p.RequiresCredential {
|
|
if p.SecretRef == "" {
|
|
return p, errors.New("secret_ref required when requires_credential is true")
|
|
}
|
|
if !strings.HasPrefix(p.SecretRef, "azkv://") {
|
|
return p, errors.New("secret_ref must use azkv:// Azure Key Vault reference")
|
|
}
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func findAgentApprovalForUser(c *gin.Context) (model.AgentApprovalRequest, bool) {
|
|
userID := c.GetInt("id")
|
|
if userID <= 0 {
|
|
common.ApiErrorMsg(c, "user authentication required")
|
|
return model.AgentApprovalRequest{}, false
|
|
}
|
|
approvalID := strings.TrimSpace(c.Param("approval_id"))
|
|
if approvalID == "" {
|
|
common.ApiErrorMsg(c, "approval_id required")
|
|
return model.AgentApprovalRequest{}, false
|
|
}
|
|
var approval model.AgentApprovalRequest
|
|
if err := model.DB.Where("approval_id = ? AND user_id = ?", approvalID, userID).First(&approval).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
common.ApiErrorMsg(c, "approval request not found")
|
|
return model.AgentApprovalRequest{}, false
|
|
}
|
|
common.ApiError(c, err)
|
|
return model.AgentApprovalRequest{}, false
|
|
}
|
|
return approval, true
|
|
}
|
|
|
|
func findAgentCredentialLeaseByApproval(approvalID string) *model.AgentCredentialLease {
|
|
var lease model.AgentCredentialLease
|
|
if err := model.DB.Where("approval_id = ?", approvalID).First(&lease).Error; err != nil {
|
|
return nil
|
|
}
|
|
expireAgentCredentialLeaseIfNeeded(&lease)
|
|
return &lease
|
|
}
|
|
|
|
func expireAgentApprovalIfNeeded(approval *model.AgentApprovalRequest) bool {
|
|
if approval == nil || approval.Status != agentApprovalStatusPending {
|
|
return false
|
|
}
|
|
if approval.ExpiresAt <= 0 || approval.ExpiresAt > time.Now().UnixMilli() {
|
|
return false
|
|
}
|
|
approval.Status = agentApprovalStatusExpired
|
|
approval.DecidedAt = time.Now().UnixMilli()
|
|
if err := model.DB.Save(approval).Error; err == nil {
|
|
recordAgentApprovalAudit("approval.expired", approval, nil, "ok", "")
|
|
}
|
|
return true
|
|
}
|
|
|
|
func expireAgentCredentialLeaseIfNeeded(lease *model.AgentCredentialLease) bool {
|
|
if lease == nil || lease.Status != agentLeaseStatusActive {
|
|
return false
|
|
}
|
|
if lease.ExpiresAt <= 0 || lease.ExpiresAt > time.Now().UnixMilli() {
|
|
return false
|
|
}
|
|
lease.Status = agentLeaseStatusExpired
|
|
if err := model.DB.Save(lease).Error; err != nil {
|
|
return false
|
|
}
|
|
var approval model.AgentApprovalRequest
|
|
if err := model.DB.Where("approval_id = ?", lease.ApprovalID).First(&approval).Error; err == nil {
|
|
recordAgentApprovalAudit("credential_lease.expired", &approval, lease, "ok", "")
|
|
}
|
|
return true
|
|
}
|
|
|
|
func agentApprovalToResponse(approval model.AgentApprovalRequest, lease *model.AgentCredentialLease) agentApprovalResponse {
|
|
resp := agentApprovalResponse{
|
|
ApprovalID: approval.ApprovalID,
|
|
UserId: approval.UserId,
|
|
DeploymentID: approval.DeploymentID,
|
|
BindingScope: approval.BindingScope,
|
|
Operation: approval.Operation,
|
|
ResourceID: approval.ResourceID,
|
|
ResourceType: approval.ResourceType,
|
|
ResourceScope: approval.ResourceScope,
|
|
TargetRole: approval.TargetRole,
|
|
RiskLevel: approval.RiskLevel,
|
|
RequiresCredential: approval.RequiresCredential,
|
|
CredentialLeaseID: approval.CredentialLeaseID,
|
|
Status: approval.Status,
|
|
RequestedBy: approval.RequestedBy,
|
|
DecidedBy: approval.DecidedBy,
|
|
RequestReason: approval.RequestReason,
|
|
DecisionReason: approval.DecisionReason,
|
|
TTLSeconds: approval.TTLSeconds,
|
|
ExpiresAt: approval.ExpiresAt,
|
|
DecidedAt: approval.DecidedAt,
|
|
CreatedAt: approval.CreatedAt,
|
|
UpdatedAt: approval.UpdatedAt,
|
|
}
|
|
if lease != nil {
|
|
leaseResp := agentLeaseToResponse(*lease)
|
|
resp.CredentialLease = &leaseResp
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func agentLeaseToResponse(lease model.AgentCredentialLease) agentCredentialLeaseResponse {
|
|
return agentCredentialLeaseResponse{
|
|
LeaseID: lease.LeaseID,
|
|
CredentialRef: lease.CredentialRef,
|
|
ApprovalID: lease.ApprovalID,
|
|
UserId: lease.UserId,
|
|
DeploymentID: lease.DeploymentID,
|
|
BindingScope: lease.BindingScope,
|
|
ResourceID: lease.ResourceID,
|
|
ResourceType: lease.ResourceType,
|
|
ResourceScope: lease.ResourceScope,
|
|
TargetRole: lease.TargetRole,
|
|
Status: lease.Status,
|
|
TTLSeconds: lease.TTLSeconds,
|
|
ExpiresAt: lease.ExpiresAt,
|
|
RevokedAt: lease.RevokedAt,
|
|
CreatedAt: lease.CreatedAt,
|
|
UpdatedAt: lease.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func recordAgentApprovalAudit(event string, approval *model.AgentApprovalRequest, lease *model.AgentCredentialLease, result string, message string) {
|
|
if approval == nil {
|
|
return
|
|
}
|
|
details := map[string]any{
|
|
"approval_id": approval.ApprovalID,
|
|
"operation": approval.Operation,
|
|
"resource_id": approval.ResourceID,
|
|
"resource_type": approval.ResourceType,
|
|
"target_role": approval.TargetRole,
|
|
"risk_level": approval.RiskLevel,
|
|
"requires_credential": approval.RequiresCredential,
|
|
}
|
|
if lease != nil {
|
|
details["lease_id"] = lease.LeaseID
|
|
details["credential_ref"] = lease.CredentialRef
|
|
details["lease_status"] = lease.Status
|
|
details["lease_expires_at"] = lease.ExpiresAt
|
|
}
|
|
if message != "" {
|
|
details["message"] = message
|
|
}
|
|
detailsJSON := "{}"
|
|
if raw, err := common.Marshal(details); err == nil {
|
|
detailsJSON = string(raw)
|
|
}
|
|
model.InsertAgentAuditEvent(&model.AgentAuditEvent{
|
|
EventID: "evt_" + common.GetUUID(),
|
|
Event: event,
|
|
Actor: "manager",
|
|
Resource: fmt.Sprintf("%s:%s", approval.ResourceType, approval.ResourceID),
|
|
UserID: fmt.Sprintf("%d", approval.UserId),
|
|
BindingScope: approval.BindingScope,
|
|
DeploymentID: approval.DeploymentID,
|
|
CorrelationID: approval.ApprovalID,
|
|
Result: result,
|
|
OccurredAt: time.Now().UnixMilli(),
|
|
DetailsJSON: detailsJSON,
|
|
})
|
|
}
|
|
|
|
func agentActorForUser(userID int) string {
|
|
return fmt.Sprintf("user:%d", userID)
|
|
}
|