Zones, address sets (IPv4 and IPv6 mixed, dynamic sets with timeouts) and services, ordered rules in the early, input, forward and output chains, port forwards with hairpin NAT and IPv6 pinholes, source NAT, redirects, and switchable built-ins (loopback, conntrack, ICMPv6, antispoofing, MSS clamping, DHCPv6 client, flowtable). Every change is validated, checked with nft -c and applied in one transaction to Housecarl's own table, inet housecarl; changes made over the network roll back unless confirmed, and the confirmed ruleset is loaded at boot by housecarl-ruleset.service. An empty configuration applies nothing. Also: hit counters and the drop log (nflog), 'hc import' of an existing nftables ruleset and 'hc takeover', the hc CLI, the REST API and the web UI, users, scoped tokens (sets:write:NAME), the audit log, the web UI's certificate through Jarl, Debian packages, docs, unit tests with real nft 1.1.3, and an e2e test with real traffic (71 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
)
|
|
|
|
// Grant gives access to one resource (see internal/auth).
|
|
type Grant struct {
|
|
Resource string `json:"resource"` // config, sets, log, audit, *
|
|
Access string `json:"access"` // read or write (write implies read)
|
|
// Sets limits a "sets" grant to these dynamic sets (none: all).
|
|
Sets []string `json:"sets,omitempty"`
|
|
}
|
|
|
|
// User is a person or a service account.
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
PasswordHash string `json:"-"`
|
|
HasPassword bool `json:"has_password"`
|
|
Admin bool `json:"admin"`
|
|
Grants []Grant `json:"grants"`
|
|
Disabled bool `json:"disabled"`
|
|
Comment string `json:"comment"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
|
}
|
|
|
|
// Token is an API token. The secret is shown once, when it is made; only
|
|
// its hash is stored.
|
|
type Token struct {
|
|
ID int64 `json:"id"`
|
|
UserID int64 `json:"user_id"`
|
|
Username string `json:"username,omitempty"`
|
|
Name string `json:"name"`
|
|
PublicID string `json:"public_id"`
|
|
SecretHash string `json:"-"`
|
|
// Grants restrict the token to part of its user's permissions; nil
|
|
// means all of them.
|
|
Grants []Grant `json:"grants"`
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
|
LastUsedIP string `json:"last_used_ip,omitempty"`
|
|
RevokedAt *time.Time `json:"revoked_at,omitempty"`
|
|
// RotatedAt is when the token last got a new secret (nil: never).
|
|
RotatedAt *time.Time `json:"rotated_at,omitempty"`
|
|
// After a rotation with a grace period, the previous secret keeps
|
|
// working until PrevExpiresAt.
|
|
PrevSecretHash string `json:"-"`
|
|
PrevExpiresAt *time.Time `json:"previous_secret_valid_until,omitempty"`
|
|
}
|
|
|
|
// IssuedAt is when the token's current secret was made: its last
|
|
// rotation, or its creation.
|
|
func (t *Token) IssuedAt() time.Time {
|
|
if t.RotatedAt != nil {
|
|
return *t.RotatedAt
|
|
}
|
|
return t.CreatedAt
|
|
}
|
|
|
|
// Session is a web UI login.
|
|
type Session struct {
|
|
IDHash string `json:"-"`
|
|
UserID int64 `json:"user_id"`
|
|
CSRFToken string `json:"-"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
LastSeenAt time.Time `json:"last_seen_at"`
|
|
IP string `json:"ip"`
|
|
UserAgent string `json:"user_agent"`
|
|
}
|
|
|
|
// AuditEvent records an action: logins, users and tokens, changes,
|
|
// dynamic set entries.
|
|
type AuditEvent struct {
|
|
ID int64 `json:"id"`
|
|
Time time.Time `json:"time"`
|
|
UserID int64 `json:"user_id,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
Auth string `json:"auth,omitempty"` // session, token, socket
|
|
TokenID int64 `json:"token_id,omitempty"`
|
|
IP string `json:"ip,omitempty"`
|
|
Action string `json:"action"`
|
|
Object string `json:"object,omitempty"`
|
|
Result string `json:"result"` // success, failure, denied
|
|
Detail string `json:"detail,omitempty"`
|
|
}
|
|
|
|
// Change is one object's change: Before is nil for a create, After is nil
|
|
// for a delete. Settings have ID 0.
|
|
type Change struct {
|
|
Kind string `json:"kind"`
|
|
ID int64 `json:"id"`
|
|
Label string `json:"label"`
|
|
Before json.RawMessage `json:"before,omitempty"`
|
|
After json.RawMessage `json:"after,omitempty"`
|
|
}
|
|
|
|
// Action describes a change in a word.
|
|
func (c Change) Action() string {
|
|
switch {
|
|
case c.Before == nil:
|
|
return "create"
|
|
case c.After == nil:
|
|
return "delete"
|
|
}
|
|
return "update"
|
|
}
|
|
|
|
// Inverse is the change that undoes c.
|
|
func (c Change) Inverse() Change {
|
|
c.Before, c.After = c.After, c.Before
|
|
return c
|
|
}
|
|
|
|
// HistoryEntry is a saved configuration change.
|
|
type HistoryEntry struct {
|
|
ID int64 `json:"id"`
|
|
Time time.Time `json:"time"`
|
|
UserID int64 `json:"user_id,omitempty"`
|
|
Username string `json:"username"`
|
|
Via string `json:"via"` // web, cli, api
|
|
Description string `json:"description"`
|
|
Changes []Change `json:"changes"`
|
|
// RevertOf is the entry this one undid; RevertedBy the entry that
|
|
// undid this one.
|
|
RevertOf int64 `json:"revert_of,omitempty"`
|
|
RevertedBy int64 `json:"reverted_by,omitempty"`
|
|
}
|