feat(firewall): implement firewall blocklist feature with client management and policy rules
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m15s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 3m59s
Introduced a comprehensive firewall blocklist feature, allowing for the management of firewall clients and their associated rules. This includes endpoints for enrolling clients, listing clients and rules, and reporting apply statuses. Enhanced the API to support firewall operations, including the ability to handle block/accept policies. Updated the documentation to reflect these changes and added necessary components in the web UI for better user interaction. Additionally, modified the agent server to support firewall failover and integrated firewall functionality into the existing architecture.
This commit is contained in:
@@ -131,6 +131,28 @@ type Backend interface {
|
||||
// Runtime log cleanup audit (filesystem ops logged per tenant).
|
||||
AppendRuntimeLogCleanupAudit(tenantID, actor, filename, action string, sizeBefore int64, sizeAfter *int64, detail map[string]any) (string, error)
|
||||
ListRuntimeLogCleanupAudit(tenantID, cursor string, limit int) ([]*RuntimeLogCleanupAudit, string, bool, error)
|
||||
|
||||
// Firewall blocklist clients and policy rules.
|
||||
ListFirewallClients(tenantID string) ([]*FirewallClient, error)
|
||||
GetFirewallClient(tenantID, id string) (*FirewallClient, error)
|
||||
CreateFirewallClient(tenantID string, in *FirewallClientCreate) (*FirewallClient, error)
|
||||
UpdateFirewallClient(tenantID, id string, patch *FirewallClientPatch) (*FirewallClient, error)
|
||||
ApproveFirewallClient(tenantID, id, approverAPIKeyID string) (*FirewallClient, error)
|
||||
RevokeFirewallClient(tenantID, id string) error
|
||||
DeleteFirewallClient(tenantID, id string) error
|
||||
LookupFirewallClientByTokenHash(hash []byte) (*FirewallClient, error)
|
||||
TouchFirewallClientLastSeen(id, source, clientIP, clientVersion string) error
|
||||
TouchFirewallClientLastApply(id, source, status, errMsg string, prefixCount, ipCount int) error
|
||||
ListActiveFirewallClientHashes() ([]FirewallClientAuthRow, error)
|
||||
ListApprovedFirewallClientsForReplication(tenantID string) ([]FirewallClientReplicationRow, error)
|
||||
|
||||
ListFirewallRules(tenantID string, clientID *string) ([]*FirewallRule, error)
|
||||
ListAllFirewallRulesForClient(tenantID, clientID string) ([]*FirewallRule, error)
|
||||
ListAllFirewallRulesForReplication(tenantID string) ([]*FirewallRule, error)
|
||||
CreateFirewallRule(tenantID string, clientID *string, in *FirewallRuleCreate) (*FirewallRule, error)
|
||||
UpdateFirewallRule(tenantID, ruleID string, patch *FirewallRulePatch) (*FirewallRule, error)
|
||||
DeleteFirewallRule(tenantID, ruleID string) error
|
||||
ReorderFirewallRules(tenantID string, clientID *string, orderedIDs []string) error
|
||||
}
|
||||
|
||||
// ASNPrefixCacheEntry is a cached RIPEstat response for one ASN.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// FirewallClient is a Linux blocklist sync client enrolled via seed.
|
||||
type FirewallClient struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
TokenPrefix string `json:"token_prefix"`
|
||||
Status string `json:"status"`
|
||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||||
LastSeenAtSource string `json:"last_seen_at_source,omitempty"`
|
||||
LastSeenIP string `json:"last_seen_ip,omitempty"`
|
||||
LastApplyAt *time.Time `json:"last_apply_at,omitempty"`
|
||||
LastApplyStatus string `json:"last_apply_status,omitempty"`
|
||||
LastApplyError string `json:"last_apply_error,omitempty"`
|
||||
LastApplyPrefixCount int `json:"last_apply_prefix_count,omitempty"`
|
||||
LastApplyIPCount int `json:"last_apply_ip_count,omitempty"`
|
||||
LastApplySource string `json:"last_apply_source,omitempty"`
|
||||
ClientVersion string `json:"client_version,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ApprovedAt *time.Time `json:"approved_at,omitempty"`
|
||||
ApprovedByAPIKeyID string `json:"approved_by_api_key_id,omitempty"`
|
||||
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
||||
}
|
||||
|
||||
// FirewallClientCreate is input for enroll (token hash supplied by caller).
|
||||
type FirewallClientCreate struct {
|
||||
Name string
|
||||
Hostname string
|
||||
TokenPrefix string
|
||||
TokenHash []byte
|
||||
ClientVersion string
|
||||
}
|
||||
|
||||
// FirewallClientPatch is a partial update for operator edits.
|
||||
type FirewallClientPatch struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Hostname *string `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
// FirewallClientAuthRow is used to build the in-process firewall token index.
|
||||
type FirewallClientAuthRow struct {
|
||||
ID string
|
||||
TenantID string
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
// FirewallClientReplicationRow is pushed to speaker agents.
|
||||
type FirewallClientReplicationRow struct {
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
TokenHashHex string `json:"token_hash_hex"`
|
||||
}
|
||||
|
||||
// FirewallRule is one block/accept policy rule.
|
||||
type FirewallRule struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
ClientID *string `json:"client_id,omitempty"`
|
||||
Priority int `json:"priority"`
|
||||
Action string `json:"action"`
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// FirewallRuleCreate is input for creating a rule.
|
||||
type FirewallRuleCreate struct {
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Action string `json:"action"`
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// FirewallRulePatch is a partial rule update.
|
||||
type FirewallRulePatch struct {
|
||||
Action *string `json:"action,omitempty"`
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
ClearCommunity bool `json:"-"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ValidFirewallAction reports whether action is block or accept.
|
||||
func ValidFirewallAction(action string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(action)) {
|
||||
case "block", "accept":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ type Memory struct {
|
||||
moduleSnapshots map[string]*moduleSnapshotRec
|
||||
asnPrefixCache map[int64]*ASNPrefixCacheEntry
|
||||
apiKeys map[string]*apiKeyRec
|
||||
firewallClients map[string]*firewallClientRec
|
||||
firewallRules map[string]*FirewallRule
|
||||
firewallHashIndex map[string]string // hex hash -> client id
|
||||
maintenancePolicies map[string]*MaintenancePolicy
|
||||
maintConfigAudit []*MaintenancePolicyConfigAudit
|
||||
runtimeLogCleanupAudit []*RuntimeLogCleanupAudit
|
||||
@@ -66,6 +69,11 @@ type apiKeyRec struct {
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
type firewallClientRec struct {
|
||||
FirewallClient
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID string
|
||||
Name string
|
||||
@@ -143,6 +151,9 @@ func NewMemory() *Memory {
|
||||
moduleSnapshots: make(map[string]*moduleSnapshotRec),
|
||||
asnPrefixCache: make(map[int64]*ASNPrefixCacheEntry),
|
||||
apiKeys: make(map[string]*apiKeyRec),
|
||||
firewallClients: make(map[string]*firewallClientRec),
|
||||
firewallRules: make(map[string]*FirewallRule),
|
||||
firewallHashIndex: make(map[string]string),
|
||||
maintenancePolicies: make(map[string]*MaintenancePolicy),
|
||||
maintConfigAudit: nil,
|
||||
runtimeLogCleanupAudit: nil,
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (m *Memory) ListFirewallClients(tenantID string) ([]*FirewallClient, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []*FirewallClient
|
||||
for _, rec := range m.firewallClients {
|
||||
if rec.TenantID == tenantID {
|
||||
out = append(out, firewallClientCopy(&rec.FirewallClient))
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) GetFirewallClient(tenantID, id string) (*FirewallClient, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return firewallClientCopy(&rec.FirewallClient), nil
|
||||
}
|
||||
|
||||
func (m *Memory) CreateFirewallClient(tenantID string, in *FirewallClientCreate) (*FirewallClient, error) {
|
||||
if in == nil || strings.TrimSpace(in.Name) == "" || len(in.TokenHash) != 32 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.tenants[tenantID]; !ok {
|
||||
return nil, ErrTenantScope
|
||||
}
|
||||
hashKey := hex.EncodeToString(in.TokenHash)
|
||||
if _, dup := m.firewallHashIndex[hashKey]; dup {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
id := uuid.NewString()
|
||||
rec := &firewallClientRec{
|
||||
FirewallClient: FirewallClient{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
Name: strings.TrimSpace(in.Name),
|
||||
Hostname: strings.TrimSpace(in.Hostname),
|
||||
TokenPrefix: in.TokenPrefix,
|
||||
Status: "pending",
|
||||
ClientVersion: strings.TrimSpace(in.ClientVersion),
|
||||
CreatedAt: now,
|
||||
},
|
||||
TokenHash: append([]byte(nil), in.TokenHash...),
|
||||
}
|
||||
m.firewallClients[id] = rec
|
||||
m.firewallHashIndex[hashKey] = id
|
||||
return firewallClientCopy(&rec.FirewallClient), nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdateFirewallClient(tenantID, id string, patch *FirewallClientPatch) (*FirewallClient, error) {
|
||||
if patch == nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if patch.Name != nil {
|
||||
n := strings.TrimSpace(*patch.Name)
|
||||
if n == "" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
rec.Name = n
|
||||
}
|
||||
if patch.Hostname != nil {
|
||||
rec.Hostname = strings.TrimSpace(*patch.Hostname)
|
||||
}
|
||||
return firewallClientCopy(&rec.FirewallClient), nil
|
||||
}
|
||||
|
||||
func (m *Memory) ApproveFirewallClient(tenantID, id, approverAPIKeyID string) (*FirewallClient, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if rec.Status == "revoked" {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.Status = "approved"
|
||||
rec.ApprovedAt = &now
|
||||
rec.ApprovedByAPIKeyID = strings.TrimSpace(approverAPIKeyID)
|
||||
rec.RevokedAt = nil
|
||||
return firewallClientCopy(&rec.FirewallClient), nil
|
||||
}
|
||||
|
||||
func (m *Memory) RevokeFirewallClient(tenantID, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return ErrNotFound
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.Status = "revoked"
|
||||
rec.RevokedAt = &now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) DeleteFirewallClient(tenantID, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok || rec.TenantID != tenantID {
|
||||
return ErrNotFound
|
||||
}
|
||||
hashKey := hex.EncodeToString(rec.TokenHash)
|
||||
delete(m.firewallHashIndex, hashKey)
|
||||
delete(m.firewallClients, id)
|
||||
for rid, rule := range m.firewallRules {
|
||||
if rule.ClientID != nil && *rule.ClientID == id {
|
||||
delete(m.firewallRules, rid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) LookupFirewallClientByTokenHash(hash []byte) (*FirewallClient, error) {
|
||||
if len(hash) != 32 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
id, ok := m.firewallHashIndex[hex.EncodeToString(hash)]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return firewallClientCopy(&rec.FirewallClient), nil
|
||||
}
|
||||
|
||||
func (m *Memory) TouchFirewallClientLastSeen(id, source, clientIP, clientVersion string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.LastSeenAt = &now
|
||||
rec.LastSeenAtSource = strings.TrimSpace(source)
|
||||
rec.LastSeenIP = strings.TrimSpace(clientIP)
|
||||
if v := strings.TrimSpace(clientVersion); v != "" {
|
||||
rec.ClientVersion = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) TouchFirewallClientLastApply(id, source, status, errMsg string, prefixCount, ipCount int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rec, ok := m.firewallClients[id]
|
||||
if !ok {
|
||||
return ErrNotFound
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
rec.LastApplyAt = &now
|
||||
rec.LastApplySource = strings.TrimSpace(source)
|
||||
rec.LastApplyStatus = strings.TrimSpace(status)
|
||||
rec.LastApplyError = strings.TrimSpace(errMsg)
|
||||
rec.LastApplyPrefixCount = prefixCount
|
||||
rec.LastApplyIPCount = ipCount
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListActiveFirewallClientHashes() ([]FirewallClientAuthRow, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []FirewallClientAuthRow
|
||||
for _, rec := range m.firewallClients {
|
||||
if rec.Status != "approved" {
|
||||
continue
|
||||
}
|
||||
out = append(out, FirewallClientAuthRow{
|
||||
ID: rec.ID,
|
||||
TenantID: rec.TenantID,
|
||||
TokenHash: append([]byte(nil), rec.TokenHash...),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListApprovedFirewallClientsForReplication(tenantID string) ([]FirewallClientReplicationRow, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []FirewallClientReplicationRow
|
||||
for _, rec := range m.firewallClients {
|
||||
if rec.TenantID != tenantID || rec.Status != "approved" {
|
||||
continue
|
||||
}
|
||||
out = append(out, FirewallClientReplicationRow{
|
||||
ClientID: rec.ID,
|
||||
Name: rec.Name,
|
||||
TokenHashHex: hex.EncodeToString(rec.TokenHash),
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ClientID < out[j].ClientID })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListFirewallRules(tenantID string, clientID *string) ([]*FirewallRule, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []*FirewallRule
|
||||
for _, rule := range m.firewallRules {
|
||||
if rule.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if clientID == nil {
|
||||
if rule.ClientID != nil {
|
||||
continue
|
||||
}
|
||||
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
|
||||
continue
|
||||
}
|
||||
out = append(out, firewallRuleCopy(rule))
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Priority < out[j].Priority })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListAllFirewallRulesForClient(tenantID, clientID string) ([]*FirewallRule, error) {
|
||||
tenantRules, err := m.ListFirewallRules(tenantID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cid := clientID
|
||||
clientRules, err := m.ListFirewallRules(tenantID, &cid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]*FirewallRule, 0, len(tenantRules)+len(clientRules))
|
||||
out = append(out, clientRules...)
|
||||
out = append(out, tenantRules...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) ListAllFirewallRulesForReplication(tenantID string) ([]*FirewallRule, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
var out []*FirewallRule
|
||||
for _, rule := range m.firewallRules {
|
||||
if rule.TenantID == tenantID {
|
||||
out = append(out, firewallRuleCopy(rule))
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
ac, bc := "", ""
|
||||
if out[i].ClientID != nil {
|
||||
ac = *out[i].ClientID
|
||||
}
|
||||
if out[j].ClientID != nil {
|
||||
bc = *out[j].ClientID
|
||||
}
|
||||
if ac != bc {
|
||||
return ac < bc
|
||||
}
|
||||
return out[i].Priority < out[j].Priority
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Memory) CreateFirewallRule(tenantID string, clientID *string, in *FirewallRuleCreate) (*FirewallRule, error) {
|
||||
if in == nil || !ValidFirewallAction(in.Action) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if _, ok := m.tenants[tenantID]; !ok {
|
||||
return nil, ErrTenantScope
|
||||
}
|
||||
if clientID != nil {
|
||||
if rec, ok := m.firewallClients[*clientID]; !ok || rec.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
}
|
||||
priority := m.nextFirewallRulePriorityLocked(tenantID, clientID)
|
||||
if in.Priority != nil && *in.Priority >= 1 {
|
||||
priority = *in.Priority
|
||||
}
|
||||
if m.firewallRulePriorityTakenLocked(tenantID, clientID, priority, "") {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
id := uuid.NewString()
|
||||
rule := &FirewallRule{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
ClientID: clientID,
|
||||
Priority: priority,
|
||||
Action: strings.ToLower(strings.TrimSpace(in.Action)),
|
||||
CommunityID: in.CommunityID,
|
||||
Comment: strings.TrimSpace(in.Comment),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
m.firewallRules[id] = rule
|
||||
return firewallRuleCopy(rule), nil
|
||||
}
|
||||
|
||||
func (m *Memory) UpdateFirewallRule(tenantID, ruleID string, patch *FirewallRulePatch) (*FirewallRule, error) {
|
||||
if patch == nil {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rule, ok := m.firewallRules[ruleID]
|
||||
if !ok || rule.TenantID != tenantID {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if patch.Action != nil {
|
||||
if !ValidFirewallAction(*patch.Action) {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
rule.Action = strings.ToLower(strings.TrimSpace(*patch.Action))
|
||||
}
|
||||
if patch.ClearCommunity {
|
||||
rule.CommunityID = nil
|
||||
} else if patch.CommunityID != nil {
|
||||
rule.CommunityID = patch.CommunityID
|
||||
}
|
||||
if patch.Comment != nil {
|
||||
rule.Comment = strings.TrimSpace(*patch.Comment)
|
||||
}
|
||||
rule.UpdatedAt = time.Now().UTC()
|
||||
return firewallRuleCopy(rule), nil
|
||||
}
|
||||
|
||||
func (m *Memory) DeleteFirewallRule(tenantID, ruleID string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
rule, ok := m.firewallRules[ruleID]
|
||||
if !ok || rule.TenantID != tenantID {
|
||||
return ErrNotFound
|
||||
}
|
||||
delete(m.firewallRules, ruleID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) ReorderFirewallRules(tenantID string, clientID *string, orderedIDs []string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
scope := make(map[string]*FirewallRule)
|
||||
for id, rule := range m.firewallRules {
|
||||
if rule.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if clientID == nil {
|
||||
if rule.ClientID != nil {
|
||||
continue
|
||||
}
|
||||
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
|
||||
continue
|
||||
}
|
||||
scope[id] = rule
|
||||
}
|
||||
if len(orderedIDs) != len(scope) {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for i, id := range orderedIDs {
|
||||
rule, ok := scope[id]
|
||||
if !ok {
|
||||
return ErrInvalidInput
|
||||
}
|
||||
rule.Priority = i + 1
|
||||
rule.UpdatedAt = now
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memory) nextFirewallRulePriorityLocked(tenantID string, clientID *string) int {
|
||||
max := 0
|
||||
for _, rule := range m.firewallRules {
|
||||
if rule.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if clientID == nil {
|
||||
if rule.ClientID != nil {
|
||||
continue
|
||||
}
|
||||
} else if rule.ClientID == nil || *rule.ClientID != *clientID {
|
||||
continue
|
||||
}
|
||||
if rule.Priority > max {
|
||||
max = rule.Priority
|
||||
}
|
||||
}
|
||||
return max + 1
|
||||
}
|
||||
|
||||
func (m *Memory) firewallRulePriorityTakenLocked(tenantID string, clientID *string, priority int, exceptID string) bool {
|
||||
for id, rule := range m.firewallRules {
|
||||
if id == exceptID || rule.TenantID != tenantID || rule.Priority != priority {
|
||||
continue
|
||||
}
|
||||
if clientID == nil {
|
||||
if rule.ClientID == nil {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if rule.ClientID != nil && *rule.ClientID == *clientID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func firewallClientCopy(c *FirewallClient) *FirewallClient {
|
||||
cp := *c
|
||||
cp.LastSeenAt = cloneTime(c.LastSeenAt)
|
||||
cp.LastApplyAt = cloneTime(c.LastApplyAt)
|
||||
cp.ApprovedAt = cloneTime(c.ApprovedAt)
|
||||
cp.RevokedAt = cloneTime(c.RevokedAt)
|
||||
return &cp
|
||||
}
|
||||
|
||||
func firewallRuleCopy(r *FirewallRule) *FirewallRule {
|
||||
cp := *r
|
||||
if r.ClientID != nil {
|
||||
v := *r.ClientID
|
||||
cp.ClientID = &v
|
||||
}
|
||||
if r.CommunityID != nil {
|
||||
v := *r.CommunityID
|
||||
cp.CommunityID = &v
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
|
||||
func cloneTime(t *time.Time) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
v := *t
|
||||
return &v
|
||||
}
|
||||
@@ -9,17 +9,21 @@ import (
|
||||
|
||||
// SpeakerMeta holds well-known keys from bgp_speaker.meta_json.
|
||||
type SpeakerMeta struct {
|
||||
AgentDomain string `json:"agent_domain,omitempty"`
|
||||
AgentSecret string `json:"agent_secret,omitempty"`
|
||||
AgentPort int `json:"agent_port,omitempty"`
|
||||
NodeIPv4 string `json:"node_ipv4,omitempty"`
|
||||
BirdBgpSourceIPv4 string `json:"bird_bgp_source_ipv4,omitempty"`
|
||||
BirdBgpSourceIPv6 string `json:"bird_bgp_source_ipv6,omitempty"`
|
||||
NodeEnrolledAt string `json:"node_enrolled_at,omitempty"`
|
||||
LastDispatchAt string `json:"last_dispatch_at,omitempty"`
|
||||
LastDispatchError string `json:"last_dispatch_error,omitempty"`
|
||||
LastDispatchStatus string `json:"last_dispatch_status,omitempty"`
|
||||
SyncStatus string `json:"sync_status,omitempty"`
|
||||
AgentDomain string `json:"agent_domain,omitempty"`
|
||||
AgentSecret string `json:"agent_secret,omitempty"`
|
||||
AgentPort int `json:"agent_port,omitempty"`
|
||||
NodeIPv4 string `json:"node_ipv4,omitempty"`
|
||||
BirdBgpSourceIPv4 string `json:"bird_bgp_source_ipv4,omitempty"`
|
||||
BirdBgpSourceIPv6 string `json:"bird_bgp_source_ipv6,omitempty"`
|
||||
NodeEnrolledAt string `json:"node_enrolled_at,omitempty"`
|
||||
LastDispatchAt string `json:"last_dispatch_at,omitempty"`
|
||||
LastDispatchError string `json:"last_dispatch_error,omitempty"`
|
||||
LastDispatchStatus string `json:"last_dispatch_status,omitempty"`
|
||||
SyncStatus string `json:"sync_status,omitempty"`
|
||||
FirewallFailover bool `json:"firewall_failover,omitempty"`
|
||||
LastFirewallReplicateAt string `json:"last_firewall_replicate_at,omitempty"`
|
||||
LastFirewallReplicateStatus string `json:"last_firewall_replicate_status,omitempty"`
|
||||
LastFirewallReplicateError string `json:"last_firewall_replicate_error,omitempty"`
|
||||
}
|
||||
|
||||
// ParseSpeakerMeta decodes meta_json object; unknown keys are ignored.
|
||||
@@ -83,6 +87,20 @@ func MergeSpeakerMetaJSON(existing string, patch SpeakerMeta) string {
|
||||
if patch.SyncStatus != "" {
|
||||
cur.SyncStatus = patch.SyncStatus
|
||||
}
|
||||
if patch.FirewallFailover {
|
||||
cur.FirewallFailover = true
|
||||
}
|
||||
if patch.LastFirewallReplicateAt != "" {
|
||||
cur.LastFirewallReplicateAt = patch.LastFirewallReplicateAt
|
||||
}
|
||||
if patch.LastFirewallReplicateStatus == "ok" {
|
||||
cur.LastFirewallReplicateError = ""
|
||||
} else if patch.LastFirewallReplicateError != "" {
|
||||
cur.LastFirewallReplicateError = patch.LastFirewallReplicateError
|
||||
}
|
||||
if patch.LastFirewallReplicateStatus != "" {
|
||||
cur.LastFirewallReplicateStatus = patch.LastFirewallReplicateStatus
|
||||
}
|
||||
return SpeakerMetaJSON(cur)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user