Beta 1
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResellerRuntimeState is a password-free ownership record replicated from a
|
||||
// master panel to its managed nodes. It lets a node enforce reseller
|
||||
// suspension and parent hierarchy locally without copying login credentials.
|
||||
type ResellerRuntimeState struct {
|
||||
OwnerUsername string
|
||||
ParentUsername string
|
||||
IsActive bool
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
type resellerRuntimeStateCacheT struct {
|
||||
mu sync.RWMutex
|
||||
m map[string]ResellerRuntimeState
|
||||
}
|
||||
|
||||
var resellerRuntimeStates = &resellerRuntimeStateCacheT{m: make(map[string]ResellerRuntimeState)}
|
||||
|
||||
func (m *resellerRuntimeStateCacheT) get(username string) (ResellerRuntimeState, bool) {
|
||||
m.mu.RLock()
|
||||
state, ok := m.m[username]
|
||||
m.mu.RUnlock()
|
||||
return state, ok
|
||||
}
|
||||
|
||||
func (m *resellerRuntimeStateCacheT) set(state ResellerRuntimeState) {
|
||||
m.mu.Lock()
|
||||
m.m[state.OwnerUsername] = state
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *resellerRuntimeStateCacheT) delete(username string) {
|
||||
m.mu.Lock()
|
||||
delete(m.m, username)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *resellerRuntimeStateCacheT) list() []ResellerRuntimeState {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]ResellerRuntimeState, 0, len(m.m))
|
||||
for _, state := range m.m {
|
||||
out = append(out, state)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *resellerRuntimeStateCacheT) replaceAll(states []ResellerRuntimeState) {
|
||||
m.mu.Lock()
|
||||
m.m = make(map[string]ResellerRuntimeState, len(states))
|
||||
for _, state := range states {
|
||||
m.m[state.OwnerUsername] = state
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Store) ListResellerRuntimeStates(ctx context.Context) ([]ResellerRuntimeState, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT owner_username, parent_username, is_active, expires_at
|
||||
FROM reseller_runtime_state`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []ResellerRuntimeState
|
||||
for rows.Next() {
|
||||
var state ResellerRuntimeState
|
||||
var expiresAt sql.NullTime
|
||||
if err := rows.Scan(&state.OwnerUsername, &state.ParentUsername, &state.IsActive, &expiresAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if expiresAt.Valid {
|
||||
state.ExpiresAt = &expiresAt.Time
|
||||
}
|
||||
out = append(out, state)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) UpsertResellerRuntimeState(ctx context.Context, state ResellerRuntimeState) error {
|
||||
var expiresAt interface{}
|
||||
if state.ExpiresAt != nil {
|
||||
expiresAt = *state.ExpiresAt
|
||||
}
|
||||
_, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO reseller_runtime_state
|
||||
(owner_username, parent_username, is_active, expires_at, updated_at)
|
||||
VALUES ($1,$2,$3,$4,NOW())
|
||||
ON CONFLICT (owner_username) DO UPDATE SET
|
||||
parent_username=EXCLUDED.parent_username,
|
||||
is_active=EXCLUDED.is_active,
|
||||
expires_at=EXCLUDED.expires_at,
|
||||
updated_at=NOW()`,
|
||||
state.OwnerUsername, state.ParentUsername, state.IsActive, expiresAt)
|
||||
if err == nil {
|
||||
resellerRuntimeStates.set(state)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) DeleteResellerRuntimeState(ctx context.Context, owner string) error {
|
||||
if _, err := s.db.ExecContext(ctx, `DELETE FROM reseller_runtime_state WHERE owner_username=$1`, owner); err != nil {
|
||||
return err
|
||||
}
|
||||
resellerRuntimeStates.delete(owner)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resellerRuntimeChainActive(username string) error {
|
||||
seen := make(map[string]bool)
|
||||
now := time.Now()
|
||||
for depth := 0; username != "" && depth < 128; depth++ {
|
||||
if seen[username] {
|
||||
return fmt.Errorf("reseller hierarchy cycle detected")
|
||||
}
|
||||
seen[username] = true
|
||||
state, ok := resellerRuntimeStates.get(username)
|
||||
if !ok {
|
||||
return fmt.Errorf("reseller runtime state not found")
|
||||
}
|
||||
if !state.IsActive {
|
||||
return fmt.Errorf("reseller account suspended")
|
||||
}
|
||||
if state.ExpiresAt != nil && now.After(*state.ExpiresAt) {
|
||||
return fmt.Errorf("reseller account expired")
|
||||
}
|
||||
username = strings.TrimSpace(state.ParentUsername)
|
||||
}
|
||||
if username != "" {
|
||||
return fmt.Errorf("reseller hierarchy is too deep")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resellerRuntimeStateFor(owner string, effectiveActive bool) (ResellerRuntimeState, error) {
|
||||
u, ok := adminUsers.get(owner)
|
||||
if !ok || u.Role != RoleReseller {
|
||||
return ResellerRuntimeState{}, fmt.Errorf("reseller account not found")
|
||||
}
|
||||
return ResellerRuntimeState{
|
||||
OwnerUsername: u.Username,
|
||||
ParentUsername: u.ParentUsername,
|
||||
IsActive: effectiveActive,
|
||||
ExpiresAt: u.ExpiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// syncOwnerChainToManagedServer makes account creation on a managed node safe:
|
||||
// every parent is installed before the child, and no password/hash is sent.
|
||||
func syncOwnerChainToManagedServer(ctx context.Context, ms *ManagedServer, owner string) error {
|
||||
var chain []*AdminUser
|
||||
seen := make(map[string]bool)
|
||||
for current := strings.TrimSpace(owner); current != ""; {
|
||||
if seen[current] {
|
||||
return fmt.Errorf("reseller hierarchy cycle detected")
|
||||
}
|
||||
seen[current] = true
|
||||
u, ok := adminUsers.get(current)
|
||||
if !ok || u.Role != RoleReseller {
|
||||
return fmt.Errorf("reseller account not found")
|
||||
}
|
||||
chain = append(chain, u)
|
||||
current = strings.TrimSpace(u.ParentUsername)
|
||||
}
|
||||
for i := len(chain) - 1; i >= 0; i-- {
|
||||
state, err := resellerRuntimeStateFor(chain[i].Username, adminAccountChainActive(chain[i].Username) == nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := resellerRuntimePayloadFromState(state, "sync")
|
||||
if err := sendResellerRuntimeToServer(ctx, ms, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncAllResellerRuntimeStates repairs legacy managed nodes after an upgrade.
|
||||
// It runs asynchronously and never prevents the local panel from starting.
|
||||
func startManagedResellerStateSync(store *Store) {
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
servers, err := store.ListManagedServers(ctx)
|
||||
if err != nil {
|
||||
log.Printf("reseller state sync: %v", err)
|
||||
return
|
||||
}
|
||||
users := adminUsers.list()
|
||||
sort.SliceStable(users, func(i, j int) bool {
|
||||
return resellerHierarchyDepth(users[i].Username) < resellerHierarchyDepth(users[j].Username)
|
||||
})
|
||||
for _, ms := range servers {
|
||||
for _, u := range users {
|
||||
if u.Role != RoleReseller {
|
||||
continue
|
||||
}
|
||||
action := "suspend"
|
||||
active := adminAccountChainActive(u.Username) == nil
|
||||
if active {
|
||||
action = "reactivate"
|
||||
}
|
||||
state, stateErr := resellerRuntimeStateFor(u.Username, active)
|
||||
if stateErr != nil {
|
||||
continue
|
||||
}
|
||||
if sendErr := sendResellerRuntimeToServer(ctx, ms, resellerRuntimePayloadFromState(state, action)); sendErr != nil {
|
||||
log.Printf("reseller state sync to %s for %s: %v", ms.Name, u.Username, sendErr)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// reconcileLocalResellerRuntimeStates reapplies replicated ownership state
|
||||
// after a managed node restarts.
|
||||
func reconcileLocalResellerRuntimeStates(store *Store) {
|
||||
if store == nil {
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, state := range resellerRuntimeStates.list() {
|
||||
action := "suspend"
|
||||
if resellerRuntimeChainActive(state.OwnerUsername) == nil {
|
||||
action = "reactivate"
|
||||
}
|
||||
if err := applyOwnerRuntimeLocal(ctx, store, state.OwnerUsername, action); err != nil {
|
||||
log.Printf("reconcile local reseller runtime for %s: %v", state.OwnerUsername, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resellerHierarchyDepth(username string) int {
|
||||
seen := make(map[string]bool)
|
||||
depth := 0
|
||||
for username != "" && depth < 128 {
|
||||
if seen[username] {
|
||||
return 128
|
||||
}
|
||||
seen[username] = true
|
||||
u, ok := adminUsers.get(username)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
depth++
|
||||
username = strings.TrimSpace(u.ParentUsername)
|
||||
}
|
||||
return depth
|
||||
}
|
||||
Reference in New Issue
Block a user