项目文件夹

文件
wehub-resource-sync 75f3dd141c
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CI and Release / lint-frontend (push) Has been cancelled
CI and Release / dockerfile-scan (push) Has been cancelled
CI and Release / test-frontend (push) Has been cancelled
CI and Release / lint-verification-agent (push) Has been cancelled
CI and Release / test-verification-agent (push) Has been cancelled
CI and Release / e2e-verification-agent (push) Has been cancelled
CI and Release / test-backend (push) Has been cancelled
CI and Release / build-dev-image (push) Has been cancelled
CI and Release / push-dev-image (push) Has been cancelled
CI and Release / build-image (push) Has been cancelled
CI and Release / build-verification-image (push) Has been cancelled
CI and Release / determine-version (push) Has been cancelled
CI and Release / push-image (push) Has been cancelled
CI and Release / push-verification-image (12) (push) Has been cancelled
CI and Release / lint-backend (push) Has been cancelled
CI and Release / push-verification-image (17) (push) Has been cancelled
CI and Release / push-verification-image (18) (push) Has been cancelled
CI and Release / release (push) Has been cancelled
CI and Release / publish-helm-chart (push) Has been cancelled
CI and Release / push-verification-image (13) (push) Has been cancelled
CI and Release / push-verification-image (14) (push) Has been cancelled
CI and Release / push-verification-image (15) (push) Has been cancelled
CI and Release / push-verification-image (16) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:11:07 +08:00

205 行
4.5 KiB
Go

package verification_agents
import (
"crypto/sha256"
"crypto/subtle"
"fmt"
"log/slog"
"strings"
"time"
"github.com/google/uuid"
audit_logs "databasus-backend/internal/features/audit_logs"
users_models "databasus-backend/internal/features/users/models"
cache_utils "databasus-backend/internal/util/cache"
)
type AgentService struct {
agentRepository *AgentRepository
auditLogService *audit_logs.AuditLogService
rateLimiter *cache_utils.RateLimiter
logger *slog.Logger
heartbeatListeners []AgentHeartbeatedListener
}
func (s *AgentService) AddAgentHeartbeatedListener(listener AgentHeartbeatedListener) {
s.heartbeatListeners = append(s.heartbeatListeners, listener)
}
func (s *AgentService) CreateAgent(
user *users_models.User,
req *CreateAgentRequest,
) (*CreatedAgentResponse, error) {
plainToken := newPlainToken()
tokenHash := hashAgentToken(plainToken)
agent := &Agent{
ID: uuid.New(),
Name: req.Name,
TokenHash: tokenHash,
CreatedAt: time.Now().UTC(),
}
if err := s.agentRepository.Create(agent); err != nil {
return nil, err
}
s.auditLogService.WriteAuditLog(
fmt.Sprintf("verification agent created: %s", agent.Name),
&user.ID,
nil,
)
return &CreatedAgentResponse{
Agent: agent,
Token: plainToken,
}, nil
}
func (s *AgentService) ListAgents() ([]*Agent, error) {
return s.agentRepository.FindAll()
}
func (s *AgentService) CountLiveAgents() (int64, error) {
return s.agentRepository.CountLive()
}
func (s *AgentService) GetStaleAgents(threshold time.Duration) ([]*Agent, error) {
return s.agentRepository.FindStale(time.Now().UTC().Add(-threshold))
}
func (s *AgentService) RotateToken(
user *users_models.User,
agentID uuid.UUID,
) (string, error) {
agent, err := s.agentRepository.FindByID(agentID)
if err != nil {
return "", err
}
if agent == nil || agent.IsDeleted() {
return "", ErrAgentNotFound
}
plainToken := newPlainToken()
tokenHash := hashAgentToken(plainToken)
if err := s.agentRepository.UpdateTokenHash(agentID, tokenHash); err != nil {
return "", err
}
s.auditLogService.WriteAuditLog(
fmt.Sprintf("verification agent token rotated: %s", agent.Name),
&user.ID,
nil,
)
return plainToken, nil
}
func (s *AgentService) DeleteAgent(user *users_models.User, agentID uuid.UUID) error {
agent, err := s.agentRepository.FindByID(agentID)
if err != nil {
return err
}
if agent == nil || agent.IsDeleted() {
return ErrAgentNotFound
}
if err := s.agentRepository.SoftDelete(agentID, time.Now().UTC()); err != nil {
return err
}
s.auditLogService.WriteAuditLog(
fmt.Sprintf("verification agent deleted: %s", agent.Name),
&user.ID,
nil,
)
return nil
}
func (s *AgentService) VerifyAgentCredentials(
agentID uuid.UUID,
rawToken string,
) (*Agent, error) {
if rawToken == "" {
return nil, ErrInvalidAgentToken
}
agent, err := s.agentRepository.FindByID(agentID)
if err != nil {
return nil, err
}
if agent == nil || agent.IsDeleted() {
return nil, ErrInvalidAgentToken
}
incomingHash := hashAgentToken(rawToken)
if subtle.ConstantTimeCompare([]byte(agent.TokenHash), []byte(incomingHash)) != 1 {
return nil, ErrInvalidAgentToken
}
return agent, nil
}
func (s *AgentService) GetAgentByID(id uuid.UUID) (*Agent, error) {
return s.agentRepository.FindByID(id)
}
func (s *AgentService) Heartbeat(
agent *Agent,
req *HeartbeatRequest,
) (seenAt time.Time, abortVerificationIDs []uuid.UUID, err error) {
now := time.Now().UTC()
err = s.agentRepository.UpdateCapacityAndLastSeen(
agent.ID,
AgentCapacity{
MaxCPU: req.MaxCPU,
MaxRAMGb: req.MaxRAMGb,
MaxDiskGb: req.MaxDiskGb,
MaxConcurrentJobs: req.MaxConcurrentJobs,
},
now,
)
if err != nil {
return time.Time{}, nil, err
}
abortSet := make(map[uuid.UUID]struct{})
for _, listener := range s.heartbeatListeners {
listenerAborts, listenerErr := listener.OnAgentHeartbeated(agent, req.CurrentVerificationIDs)
if listenerErr != nil {
s.logger.Error(
"agent heartbeated listener failed",
"error", listenerErr,
"agent_id", agent.ID,
)
continue
}
for _, id := range listenerAborts {
abortSet[id] = struct{}{}
}
}
abortVerificationIDs = make([]uuid.UUID, 0, len(abortSet))
for id := range abortSet {
abortVerificationIDs = append(abortVerificationIDs, id)
}
return now, abortVerificationIDs, nil
}
func newPlainToken() string {
return strings.ReplaceAll(uuid.New().String(), "-", "")
}
func hashAgentToken(token string) string {
hash := sha256.Sum256([]byte(token))
return fmt.Sprintf("%x", hash)
}