mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 22:58:52 +00:00
feat: M11b — ownership tracking, agent groups, interactive renewal approval
Ownership: owners/teams GUI pages, notification email resolution via resolveRecipient (owner_id → owner.email lookup). Agent groups: dynamic device grouping by OS/arch/IP CIDR/version with manual include/exclude membership, migration 000004, full CRUD stack (domain → repo → service → handler → frontend). Interactive approval: AwaitingApproval job state, approve/reject API endpoints with reason tracking. Tests: 12 agent group handler tests, 8 approve/reject job handler tests, integration tests updated for 13-param RegisterHandlers. Docs updated across architecture, concepts, and seed data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
)
|
||||
|
||||
// AgentGroupService provides business logic for agent group management.
|
||||
type AgentGroupService struct {
|
||||
groupRepo repository.AgentGroupRepository
|
||||
auditService *AuditService
|
||||
}
|
||||
|
||||
// NewAgentGroupService creates a new agent group service.
|
||||
func NewAgentGroupService(
|
||||
groupRepo repository.AgentGroupRepository,
|
||||
auditService *AuditService,
|
||||
) *AgentGroupService {
|
||||
return &AgentGroupService{
|
||||
groupRepo: groupRepo,
|
||||
auditService: auditService,
|
||||
}
|
||||
}
|
||||
|
||||
// ListAgentGroups returns paginated agent groups (handler interface method).
|
||||
func (s *AgentGroupService) ListAgentGroups(page, perPage int) ([]domain.AgentGroup, int64, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if perPage < 1 {
|
||||
perPage = 50
|
||||
}
|
||||
|
||||
groups, err := s.groupRepo.List(context.Background())
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list agent groups: %w", err)
|
||||
}
|
||||
total := int64(len(groups))
|
||||
|
||||
var result []domain.AgentGroup
|
||||
for _, g := range groups {
|
||||
if g != nil {
|
||||
result = append(result, *g)
|
||||
}
|
||||
}
|
||||
|
||||
return result, total, nil
|
||||
}
|
||||
|
||||
// GetAgentGroup returns a single agent group (handler interface method).
|
||||
func (s *AgentGroupService) GetAgentGroup(id string) (*domain.AgentGroup, error) {
|
||||
return s.groupRepo.Get(context.Background(), id)
|
||||
}
|
||||
|
||||
// CreateAgentGroup creates a new agent group with validation (handler interface method).
|
||||
func (s *AgentGroupService) CreateAgentGroup(group domain.AgentGroup) (*domain.AgentGroup, error) {
|
||||
if err := validateAgentGroup(&group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if group.ID == "" {
|
||||
group.ID = generateID("ag")
|
||||
}
|
||||
now := time.Now()
|
||||
if group.CreatedAt.IsZero() {
|
||||
group.CreatedAt = now
|
||||
}
|
||||
if group.UpdatedAt.IsZero() {
|
||||
group.UpdatedAt = now
|
||||
}
|
||||
|
||||
if err := s.groupRepo.Create(context.Background(), &group); err != nil {
|
||||
return nil, fmt.Errorf("failed to create agent group: %w", err)
|
||||
}
|
||||
|
||||
if s.auditService != nil {
|
||||
if auditErr := s.auditService.RecordEvent(context.Background(), "api", domain.ActorTypeUser,
|
||||
"create_agent_group", "agent_group", group.ID, nil); auditErr != nil {
|
||||
slog.Error("failed to record audit event", "error", auditErr)
|
||||
}
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// UpdateAgentGroup modifies an existing agent group (handler interface method).
|
||||
func (s *AgentGroupService) UpdateAgentGroup(id string, group domain.AgentGroup) (*domain.AgentGroup, error) {
|
||||
if err := validateAgentGroup(&group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.ID = id
|
||||
if err := s.groupRepo.Update(context.Background(), &group); err != nil {
|
||||
return nil, fmt.Errorf("failed to update agent group: %w", err)
|
||||
}
|
||||
|
||||
if s.auditService != nil {
|
||||
if auditErr := s.auditService.RecordEvent(context.Background(), "api", domain.ActorTypeUser,
|
||||
"update_agent_group", "agent_group", id, nil); auditErr != nil {
|
||||
slog.Error("failed to record audit event", "error", auditErr)
|
||||
}
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
|
||||
// DeleteAgentGroup removes an agent group (handler interface method).
|
||||
func (s *AgentGroupService) DeleteAgentGroup(id string) error {
|
||||
if err := s.groupRepo.Delete(context.Background(), id); err != nil {
|
||||
return fmt.Errorf("failed to delete agent group: %w", err)
|
||||
}
|
||||
|
||||
if s.auditService != nil {
|
||||
if auditErr := s.auditService.RecordEvent(context.Background(), "api", domain.ActorTypeUser,
|
||||
"delete_agent_group", "agent_group", id, nil); auditErr != nil {
|
||||
slog.Error("failed to record audit event", "error", auditErr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListMembers returns agents in a group.
|
||||
func (s *AgentGroupService) ListMembers(id string) ([]domain.Agent, int64, error) {
|
||||
agents, err := s.groupRepo.ListMembers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list group members: %w", err)
|
||||
}
|
||||
|
||||
var result []domain.Agent
|
||||
for _, a := range agents {
|
||||
if a != nil {
|
||||
result = append(result, *a)
|
||||
}
|
||||
}
|
||||
|
||||
return result, int64(len(result)), nil
|
||||
}
|
||||
|
||||
// validateAgentGroup checks that an agent group's configuration is valid.
|
||||
func validateAgentGroup(g *domain.AgentGroup) error {
|
||||
if g.Name == "" {
|
||||
return fmt.Errorf("agent group name is required")
|
||||
}
|
||||
if len(g.Name) > 255 {
|
||||
return fmt.Errorf("agent group name exceeds 255 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user