manager.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package auth
  2. import (
  3. "crypto/rand"
  4. "encoding/hex"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "log"
  9. "os"
  10. "path/filepath"
  11. "strings"
  12. "sync"
  13. "time"
  14. "golang.org/x/crypto/bcrypt"
  15. )
  16. var (
  17. ErrInvalidCredentials = errors.New("invalid username or password")
  18. ErrUserDisabled = errors.New("user account is disabled")
  19. ErrUserNotFound = errors.New("user not found")
  20. ErrUserExists = errors.New("username already exists")
  21. ErrCannotDeleteSelf = errors.New("cannot delete currently authenticated user")
  22. ErrCannotDeleteAdmin = errors.New("cannot delete or disable the last active administrator")
  23. ErrInvalidUsername = errors.New("username must be at least 3 characters and contain only alphanumeric characters, dashes or underscores")
  24. ErrPasswordTooShort = errors.New("password must be at least 4 characters")
  25. ErrSessionExpired = errors.New("session expired")
  26. )
  27. const (
  28. DefaultUsersPath = "/etc/proxmox-router/users.json"
  29. SessionDuration = 7 * 24 * time.Hour
  30. )
  31. type Manager struct {
  32. mu sync.RWMutex
  33. filePath string
  34. users map[string]User
  35. sessions map[string]Session
  36. userSession map[string][]string // username -> tokens
  37. }
  38. func NewManager(customPath string) *Manager {
  39. p := customPath
  40. if p == "" {
  41. p = DefaultUsersPath
  42. }
  43. m := &Manager{
  44. filePath: p,
  45. users: make(map[string]User),
  46. sessions: make(map[string]Session),
  47. userSession: make(map[string][]string),
  48. }
  49. if err := m.load(); err != nil {
  50. log.Printf("[Auth] Failed to load users, initializing default: %v", err)
  51. _ = m.initDefaultAdmin()
  52. }
  53. // Start background session cleanup
  54. go m.cleanupWorker()
  55. return m
  56. }
  57. func (m *Manager) initDefaultAdmin() error {
  58. m.mu.Lock()
  59. defer m.mu.Unlock()
  60. if len(m.users) == 0 {
  61. hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
  62. if err != nil {
  63. return err
  64. }
  65. admin := User{
  66. Username: "admin",
  67. PasswordHash: string(hash),
  68. Role: RoleFull,
  69. Comment: "Default System Administrator",
  70. Disabled: false,
  71. CreatedAt: time.Now(),
  72. }
  73. m.users["admin"] = admin
  74. return m.saveLocked()
  75. }
  76. return nil
  77. }
  78. func (m *Manager) load() error {
  79. m.mu.Lock()
  80. defer m.mu.Unlock()
  81. data, err := os.ReadFile(m.filePath)
  82. if err != nil {
  83. if os.IsNotExist(err) {
  84. return m.initDefaultAdminLocked()
  85. }
  86. return err
  87. }
  88. var list []User
  89. if err := json.Unmarshal(data, &list); err != nil {
  90. return err
  91. }
  92. m.users = make(map[string]User)
  93. for _, u := range list {
  94. m.users[u.Username] = u
  95. }
  96. if len(m.users) == 0 {
  97. return m.initDefaultAdminLocked()
  98. }
  99. return nil
  100. }
  101. func (m *Manager) initDefaultAdminLocked() error {
  102. hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
  103. if err != nil {
  104. return err
  105. }
  106. admin := User{
  107. Username: "admin",
  108. PasswordHash: string(hash),
  109. Role: RoleFull,
  110. Comment: "Default System Administrator",
  111. Disabled: false,
  112. CreatedAt: time.Now(),
  113. }
  114. m.users["admin"] = admin
  115. return m.saveLocked()
  116. }
  117. func (m *Manager) saveLocked() error {
  118. dir := filepath.Dir(m.filePath)
  119. if err := os.MkdirAll(dir, 0755); err != nil {
  120. return err
  121. }
  122. list := make([]User, 0, len(m.users))
  123. for _, u := range m.users {
  124. list = append(list, u)
  125. }
  126. data, err := json.MarshalIndent(list, "", " ")
  127. if err != nil {
  128. return err
  129. }
  130. tmpFile := filepath.Join(dir, fmt.Sprintf(".users.%d.tmp", time.Now().UnixNano()))
  131. if err := os.WriteFile(tmpFile, data, 0600); err != nil {
  132. return err
  133. }
  134. return os.Rename(tmpFile, m.filePath)
  135. }
  136. func (m *Manager) Authenticate(username, password string) (*Session, error) {
  137. m.mu.Lock()
  138. defer m.mu.Unlock()
  139. u, exists := m.users[username]
  140. if !exists {
  141. return nil, ErrInvalidCredentials
  142. }
  143. if u.Disabled {
  144. return nil, ErrUserDisabled
  145. }
  146. if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
  147. return nil, ErrInvalidCredentials
  148. }
  149. // Update last login timestamp
  150. u.LastLoginAt = time.Now()
  151. m.users[username] = u
  152. _ = m.saveLocked()
  153. // Generate session token
  154. tokenBytes := make([]byte, 32)
  155. if _, err := rand.Read(tokenBytes); err != nil {
  156. return nil, fmt.Errorf("failed to generate session token: %w", err)
  157. }
  158. token := hex.EncodeToString(tokenBytes)
  159. sess := Session{
  160. Token: token,
  161. Username: u.Username,
  162. Role: u.Role,
  163. ExpiresAt: time.Now().Add(SessionDuration),
  164. }
  165. m.sessions[token] = sess
  166. m.userSession[username] = append(m.userSession[username], token)
  167. return &sess, nil
  168. }
  169. func (m *Manager) ValidateSession(token string) (*Session, error) {
  170. if token == "" {
  171. return nil, ErrSessionExpired
  172. }
  173. m.mu.RLock()
  174. defer m.mu.RUnlock()
  175. sess, exists := m.sessions[token]
  176. if !exists {
  177. return nil, ErrSessionExpired
  178. }
  179. if time.Now().After(sess.ExpiresAt) {
  180. return nil, ErrSessionExpired
  181. }
  182. // Ensure user is still valid and not disabled
  183. u, exists := m.users[sess.Username]
  184. if !exists || u.Disabled {
  185. return nil, ErrUserDisabled
  186. }
  187. return &sess, nil
  188. }
  189. func (m *Manager) RevokeSession(token string) {
  190. m.mu.Lock()
  191. defer m.mu.Unlock()
  192. if sess, ok := m.sessions[token]; ok {
  193. delete(m.sessions, token)
  194. // Clean from userSession
  195. tokens := m.userSession[sess.Username]
  196. filtered := make([]string, 0, len(tokens))
  197. for _, t := range tokens {
  198. if t != token {
  199. filtered = append(filtered, t)
  200. }
  201. }
  202. m.userSession[sess.Username] = filtered
  203. }
  204. }
  205. func (m *Manager) ListUsers() []PublicUser {
  206. m.mu.RLock()
  207. defer m.mu.RUnlock()
  208. res := make([]PublicUser, 0, len(m.users))
  209. for _, u := range m.users {
  210. res = append(res, PublicUser{
  211. Username: u.Username,
  212. Role: u.Role,
  213. Comment: u.Comment,
  214. Disabled: u.Disabled,
  215. CreatedAt: u.CreatedAt,
  216. LastLoginAt: u.LastLoginAt,
  217. })
  218. }
  219. return res
  220. }
  221. func (m *Manager) GetPublicUser(username string) (*PublicUser, error) {
  222. m.mu.RLock()
  223. defer m.mu.RUnlock()
  224. u, exists := m.users[username]
  225. if !exists {
  226. return nil, ErrUserNotFound
  227. }
  228. return &PublicUser{
  229. Username: u.Username,
  230. Role: u.Role,
  231. Comment: u.Comment,
  232. Disabled: u.Disabled,
  233. CreatedAt: u.CreatedAt,
  234. LastLoginAt: u.LastLoginAt,
  235. }, nil
  236. }
  237. func validateUsername(name string) error {
  238. name = strings.TrimSpace(name)
  239. if len(name) < 3 {
  240. return ErrInvalidUsername
  241. }
  242. for _, c := range name {
  243. if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') {
  244. return ErrInvalidUsername
  245. }
  246. }
  247. return nil
  248. }
  249. func (m *Manager) CreateUser(req CreateUserRequest) error {
  250. if err := validateUsername(req.Username); err != nil {
  251. return err
  252. }
  253. if len(req.Password) < 4 {
  254. return ErrPasswordTooShort
  255. }
  256. role := req.Role
  257. if role == "" {
  258. role = RoleRead
  259. }
  260. m.mu.Lock()
  261. defer m.mu.Unlock()
  262. if _, exists := m.users[req.Username]; exists {
  263. return ErrUserExists
  264. }
  265. hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  266. if err != nil {
  267. return fmt.Errorf("failed to hash password: %w", err)
  268. }
  269. u := User{
  270. Username: req.Username,
  271. PasswordHash: string(hash),
  272. Role: role,
  273. Comment: req.Comment,
  274. Disabled: false,
  275. CreatedAt: time.Now(),
  276. }
  277. m.users[req.Username] = u
  278. return m.saveLocked()
  279. }
  280. func (m *Manager) UpdateUser(username string, req UpdateUserRequest) error {
  281. m.mu.Lock()
  282. defer m.mu.Unlock()
  283. u, exists := m.users[username]
  284. if !exists {
  285. return ErrUserNotFound
  286. }
  287. // Safeguard: Check if disabling or demoting the last active administrator
  288. if (req.Disabled || req.Role != RoleFull) && u.Role == RoleFull && !u.Disabled {
  289. activeAdmins := 0
  290. for _, other := range m.users {
  291. if other.Role == RoleFull && !other.Disabled {
  292. activeAdmins++
  293. }
  294. }
  295. if activeAdmins <= 1 {
  296. return ErrCannotDeleteAdmin
  297. }
  298. }
  299. if req.Role != "" {
  300. u.Role = req.Role
  301. }
  302. u.Comment = req.Comment
  303. u.Disabled = req.Disabled
  304. m.users[username] = u
  305. return m.saveLocked()
  306. }
  307. func (m *Manager) DeleteUser(username, actorUsername string) error {
  308. if username == actorUsername {
  309. return ErrCannotDeleteSelf
  310. }
  311. m.mu.Lock()
  312. defer m.mu.Unlock()
  313. u, exists := m.users[username]
  314. if !exists {
  315. return ErrUserNotFound
  316. }
  317. // Safeguard: do not delete the last active admin
  318. if u.Role == RoleFull && !u.Disabled {
  319. activeAdmins := 0
  320. for _, other := range m.users {
  321. if other.Role == RoleFull && !other.Disabled {
  322. activeAdmins++
  323. }
  324. }
  325. if activeAdmins <= 1 {
  326. return ErrCannotDeleteAdmin
  327. }
  328. }
  329. delete(m.users, username)
  330. // Invalidate any active sessions of this user
  331. for _, token := range m.userSession[username] {
  332. delete(m.sessions, token)
  333. }
  334. delete(m.userSession, username)
  335. return m.saveLocked()
  336. }
  337. func (m *Manager) ChangePassword(username, oldPassword, newPassword string, isAdmin bool) error {
  338. if len(newPassword) < 4 {
  339. return ErrPasswordTooShort
  340. }
  341. m.mu.Lock()
  342. defer m.mu.Unlock()
  343. u, exists := m.users[username]
  344. if !exists {
  345. return ErrUserNotFound
  346. }
  347. if !isAdmin {
  348. if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(oldPassword)); err != nil {
  349. return errors.New("current password incorrect")
  350. }
  351. }
  352. hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
  353. if err != nil {
  354. return fmt.Errorf("failed to hash password: %w", err)
  355. }
  356. u.PasswordHash = string(hash)
  357. m.users[username] = u
  358. return m.saveLocked()
  359. }
  360. func (m *Manager) cleanupWorker() {
  361. ticker := time.NewTicker(30 * time.Minute)
  362. defer ticker.Stop()
  363. for range ticker.C {
  364. m.mu.Lock()
  365. now := time.Now()
  366. for token, sess := range m.sessions {
  367. if now.After(sess.ExpiresAt) {
  368. delete(m.sessions, token)
  369. }
  370. }
  371. // Clean up expired tokens from userSession tracking map to prevent memory leak
  372. for username, tokens := range m.userSession {
  373. var activeTokens []string
  374. for _, t := range tokens {
  375. if _, ok := m.sessions[t]; ok {
  376. activeTokens = append(activeTokens, t)
  377. }
  378. }
  379. if len(activeTokens) == 0 {
  380. delete(m.userSession, username)
  381. } else {
  382. m.userSession[username] = activeTokens
  383. }
  384. }
  385. m.mu.Unlock()
  386. }
  387. }