feat: audit api, sdwan persist, relay fallback updates
This commit is contained in:
@@ -27,6 +27,18 @@ type Tenant struct {
|
||||
CreatedAt int64
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
ID int64 `json:"id"`
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID string `json:"actor_id"`
|
||||
Action string `json:"action"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID string `json:"target_id"`
|
||||
Detail string `json:"detail"`
|
||||
IP string `json:"ip"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
@@ -102,6 +114,12 @@ func Open(dbPath string) (*Store, error) {
|
||||
if err := s.ensureSubnetPool(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.ensureSettings(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.backfillNodeIdentity(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -182,6 +200,11 @@ func (s *Store) migrate() error {
|
||||
ip TEXT,
|
||||
created_at INTEGER NOT NULL
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS system_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);`,
|
||||
`CREATE TABLE IF NOT EXISTS subnet_pool (
|
||||
subnet TEXT PRIMARY KEY,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -282,11 +305,11 @@ func (s *Store) CreateTenantWithUsers(name, adminPassword, operatorPassword stri
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
admin, err := s.CreateUser(ten.ID, "admin", "admin@local", adminPassword, 1)
|
||||
admin, err := s.CreateUser(ten.ID, "admin", "admin@"+name, adminPassword, 1)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
op, err := s.CreateUser(ten.ID, "operator", "operator@local", operatorPassword, 1)
|
||||
op, err := s.CreateUser(ten.ID, "operator", "operator@"+name, operatorPassword, 1)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -494,6 +517,88 @@ func (s *Store) IncEnrollAttempt(code string) {
|
||||
_, _ = s.DB.Exec(`UPDATE enroll_tokens SET attempts=attempts+1 WHERE token_hash=?`, h)
|
||||
}
|
||||
|
||||
func (s *Store) ensureSettings() error {
|
||||
defaults := map[string]string{
|
||||
"advanced_impersonate": "0",
|
||||
"advanced_force_network": "0",
|
||||
"advanced_cross_tenant": "0",
|
||||
}
|
||||
now := time.Now().Unix()
|
||||
for k, v := range defaults {
|
||||
_, _ = s.DB.Exec(`INSERT OR IGNORE INTO system_settings(key,value,updated_at) VALUES(?,?,?)`, k, v, now)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSetting(key string) (string, bool, error) {
|
||||
row := s.DB.QueryRow(`SELECT value FROM system_settings WHERE key=?`, key)
|
||||
var v string
|
||||
if err := row.Scan(&v); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return v, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListSettings() (map[string]string, error) {
|
||||
rows, err := s.DB.Query(`SELECT key,value FROM system_settings`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := map[string]string{}
|
||||
for rows.Next() {
|
||||
var k, v string
|
||||
if err := rows.Scan(&k, &v); err == nil {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetSetting(key, value string) error {
|
||||
now := time.Now().Unix()
|
||||
_, err := s.DB.Exec(`INSERT INTO system_settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, key, value, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) AddAuditLog(actorType, actorID, action, targetType, targetID, detail, ip string) error {
|
||||
now := time.Now().Unix()
|
||||
_, err := s.DB.Exec(`INSERT INTO audit_logs(actor_type,actor_id,action,target_type,target_id,detail,ip,created_at) VALUES(?,?,?,?,?,?,?,?)`, actorType, actorID, action, targetType, targetID, detail, ip, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) ListAuditLogs(tenantID int64, limit, offset int) ([]AuditLog, error) {
|
||||
q := `SELECT id,actor_type,actor_id,action,target_type,target_id,detail,ip,created_at FROM audit_logs`
|
||||
args := []any{}
|
||||
if tenantID > 0 {
|
||||
// limit to logs related to this tenant
|
||||
q += ` WHERE (target_type='tenant' AND target_id=?)`
|
||||
args = append(args, fmt.Sprintf("%d", tenantID))
|
||||
}
|
||||
q += ` ORDER BY id DESC`
|
||||
if limit > 0 {
|
||||
q += ` LIMIT ?`
|
||||
args = append(args, limit)
|
||||
}
|
||||
if offset > 0 {
|
||||
q += ` OFFSET ?`
|
||||
args = append(args, offset)
|
||||
}
|
||||
rows, err := s.DB.Query(q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []AuditLog{}
|
||||
for rows.Next() {
|
||||
var a AuditLog
|
||||
if err := rows.Scan(&a.ID, &a.ActorType, &a.ActorID, &a.Action, &a.TargetType, &a.TargetID, &a.Detail, &a.IP, &a.CreatedAt); err == nil {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ListTenants returns all tenants (admin)
|
||||
func (s *Store) ListTenants() ([]Tenant, error) {
|
||||
rows, err := s.DB.Query(`SELECT id,name,status,subnet,created_at FROM tenants ORDER BY id DESC`)
|
||||
@@ -662,6 +767,15 @@ func (s *Store) UserEmailExists(tenantID int64, email string) (bool, error) {
|
||||
return c > 0, nil
|
||||
}
|
||||
|
||||
func (s *Store) UserEmailExistsGlobal(email string) (bool, error) {
|
||||
row := s.DB.QueryRow(`SELECT COUNT(1) FROM users WHERE email=?`, email)
|
||||
var c int
|
||||
if err := row.Scan(&c); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return c > 0, nil
|
||||
}
|
||||
|
||||
func (s *Store) VerifyUserPassword(tenantID int64, email, password string) (*User, error) {
|
||||
u, err := s.GetUserByEmail(tenantID, email)
|
||||
if err != nil {
|
||||
@@ -682,6 +796,21 @@ func (s *Store) VerifyUserPassword(tenantID int64, email, password string) (*Use
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (s *Store) VerifyUserPasswordGlobal(email, password string) (*User, error) {
|
||||
row := s.DB.QueryRow(`SELECT id,tenant_id,role,email,password_hash,status,created_at FROM users WHERE email=? ORDER BY id LIMIT 1`, email)
|
||||
var u User
|
||||
if err := row.Scan(&u.ID, &u.TenantID, &u.Role, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.PasswordHash == "" {
|
||||
return nil, errors.New("password not set")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid password")
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateSessionToken(userID, tenantID int64, role string, ttl time.Duration) (string, int64, error) {
|
||||
tok := randToken()
|
||||
h := hashTokenString(tok)
|
||||
@@ -742,6 +871,11 @@ func (s *Store) UpdateUserPassword(id int64, password string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) UpdateUserEmail(id int64, email string) error {
|
||||
_, err := s.DB.Exec(`UPDATE users SET email=? WHERE id=?`, email, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func hashTokenBytes(b []byte) string {
|
||||
h := sha256.Sum256(b)
|
||||
return hex.EncodeToString(h[:])
|
||||
@@ -767,5 +901,40 @@ func randUUID() string {
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Store) backfillNodeIdentity() error {
|
||||
rows, err := s.DB.Query(`SELECT id,tenant_id,node_uuid,virtual_ip FROM nodes ORDER BY id`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
type rowNode struct {
|
||||
id int64
|
||||
tenantID int64
|
||||
uuid string
|
||||
vip string
|
||||
}
|
||||
var list []rowNode
|
||||
for rows.Next() {
|
||||
var r rowNode
|
||||
if err := rows.Scan(&r.id, &r.tenantID, &r.uuid, &r.vip); err == nil {
|
||||
list = append(list, r)
|
||||
}
|
||||
}
|
||||
for _, n := range list {
|
||||
if n.uuid == "" {
|
||||
if _, err := s.DB.Exec(`UPDATE nodes SET node_uuid=? WHERE id=?`, randUUID(), n.id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(n.vip) == "" {
|
||||
vip, err := s.AllocateNodeIP(n.tenantID)
|
||||
if err == nil && vip != "" {
|
||||
_, _ = s.DB.Exec(`UPDATE nodes SET virtual_ip=? WHERE id=?`, vip, n.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// helper to avoid unused import (net)
|
||||
var _ = net.IPv4len
|
||||
|
||||
Reference in New Issue
Block a user