manager.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  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. if err := os.MkdirAll(filepath.Dir(m.filePath), 0755); err != nil {
  119. return err
  120. }
  121. list := make([]User, 0, len(m.users))
  122. for _, u := range m.users {
  123. list = append(list, u)
  124. }
  125. data, err := json.MarshalIndent(list, "", " ")
  126. if err != nil {
  127. return err
  128. }
  129. return os.WriteFile(m.filePath, data, 0600)
  130. }
  131. func (m *Manager) Authenticate(username, password string) (*Session, error) {
  132. m.mu.Lock()
  133. defer m.mu.Unlock()
  134. u, exists := m.users[username]
  135. if !exists {
  136. return nil, ErrInvalidCredentials
  137. }
  138. if u.Disabled {
  139. return nil, ErrUserDisabled
  140. }
  141. if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); err != nil {
  142. return nil, ErrInvalidCredentials
  143. }
  144. // Update last login timestamp
  145. u.LastLoginAt = time.Now()
  146. m.users[username] = u
  147. _ = m.saveLocked()
  148. // Generate session token
  149. tokenBytes := make([]byte, 32)
  150. if _, err := rand.Read(tokenBytes); err != nil {
  151. return nil, fmt.Errorf("failed to generate session token: %w", err)
  152. }
  153. token := hex.EncodeToString(tokenBytes)
  154. sess := Session{
  155. Token: token,
  156. Username: u.Username,
  157. Role: u.Role,
  158. ExpiresAt: time.Now().Add(SessionDuration),
  159. }
  160. m.sessions[token] = sess
  161. m.userSession[username] = append(m.userSession[username], token)
  162. return &sess, nil
  163. }
  164. func (m *Manager) ValidateSession(token string) (*Session, error) {
  165. if token == "" {
  166. return nil, ErrSessionExpired
  167. }
  168. m.mu.RLock()
  169. defer m.mu.RUnlock()
  170. sess, exists := m.sessions[token]
  171. if !exists {
  172. return nil, ErrSessionExpired
  173. }
  174. if time.Now().After(sess.ExpiresAt) {
  175. return nil, ErrSessionExpired
  176. }
  177. // Ensure user is still valid and not disabled
  178. u, exists := m.users[sess.Username]
  179. if !exists || u.Disabled {
  180. return nil, ErrUserDisabled
  181. }
  182. return &sess, nil
  183. }
  184. func (m *Manager) RevokeSession(token string) {
  185. m.mu.Lock()
  186. defer m.mu.Unlock()
  187. if sess, ok := m.sessions[token]; ok {
  188. delete(m.sessions, token)
  189. // Clean from userSession
  190. tokens := m.userSession[sess.Username]
  191. filtered := make([]string, 0, len(tokens))
  192. for _, t := range tokens {
  193. if t != token {
  194. filtered = append(filtered, t)
  195. }
  196. }
  197. m.userSession[sess.Username] = filtered
  198. }
  199. }
  200. func (m *Manager) ListUsers() []PublicUser {
  201. m.mu.RLock()
  202. defer m.mu.RUnlock()
  203. res := make([]PublicUser, 0, len(m.users))
  204. for _, u := range m.users {
  205. res = append(res, PublicUser{
  206. Username: u.Username,
  207. Role: u.Role,
  208. Comment: u.Comment,
  209. Disabled: u.Disabled,
  210. CreatedAt: u.CreatedAt,
  211. LastLoginAt: u.LastLoginAt,
  212. })
  213. }
  214. return res
  215. }
  216. func (m *Manager) GetPublicUser(username string) (*PublicUser, error) {
  217. m.mu.RLock()
  218. defer m.mu.RUnlock()
  219. u, exists := m.users[username]
  220. if !exists {
  221. return nil, ErrUserNotFound
  222. }
  223. return &PublicUser{
  224. Username: u.Username,
  225. Role: u.Role,
  226. Comment: u.Comment,
  227. Disabled: u.Disabled,
  228. CreatedAt: u.CreatedAt,
  229. LastLoginAt: u.LastLoginAt,
  230. }, nil
  231. }
  232. func validateUsername(name string) error {
  233. name = strings.TrimSpace(name)
  234. if len(name) < 3 {
  235. return ErrInvalidUsername
  236. }
  237. for _, c := range name {
  238. if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') {
  239. return ErrInvalidUsername
  240. }
  241. }
  242. return nil
  243. }
  244. func (m *Manager) CreateUser(req CreateUserRequest) error {
  245. if err := validateUsername(req.Username); err != nil {
  246. return err
  247. }
  248. if len(req.Password) < 4 {
  249. return ErrPasswordTooShort
  250. }
  251. role := req.Role
  252. if role == "" {
  253. role = RoleRead
  254. }
  255. m.mu.Lock()
  256. defer m.mu.Unlock()
  257. if _, exists := m.users[req.Username]; exists {
  258. return ErrUserExists
  259. }
  260. hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
  261. if err != nil {
  262. return fmt.Errorf("failed to hash password: %w", err)
  263. }
  264. u := User{
  265. Username: req.Username,
  266. PasswordHash: string(hash),
  267. Role: role,
  268. Comment: req.Comment,
  269. Disabled: false,
  270. CreatedAt: time.Now(),
  271. }
  272. m.users[req.Username] = u
  273. return m.saveLocked()
  274. }
  275. func (m *Manager) UpdateUser(username string, req UpdateUserRequest) error {
  276. m.mu.Lock()
  277. defer m.mu.Unlock()
  278. u, exists := m.users[username]
  279. if !exists {
  280. return ErrUserNotFound
  281. }
  282. // Safeguard: Check if disabling or demoting the last active administrator
  283. if (req.Disabled || req.Role != RoleFull) && u.Role == RoleFull && !u.Disabled {
  284. activeAdmins := 0
  285. for _, other := range m.users {
  286. if other.Role == RoleFull && !other.Disabled {
  287. activeAdmins++
  288. }
  289. }
  290. if activeAdmins <= 1 {
  291. return ErrCannotDeleteAdmin
  292. }
  293. }
  294. if req.Role != "" {
  295. u.Role = req.Role
  296. }
  297. u.Comment = req.Comment
  298. u.Disabled = req.Disabled
  299. m.users[username] = u
  300. return m.saveLocked()
  301. }
  302. func (m *Manager) DeleteUser(username, actorUsername string) error {
  303. if username == actorUsername {
  304. return ErrCannotDeleteSelf
  305. }
  306. m.mu.Lock()
  307. defer m.mu.Unlock()
  308. u, exists := m.users[username]
  309. if !exists {
  310. return ErrUserNotFound
  311. }
  312. // Safeguard: do not delete the last active admin
  313. if u.Role == RoleFull && !u.Disabled {
  314. activeAdmins := 0
  315. for _, other := range m.users {
  316. if other.Role == RoleFull && !other.Disabled {
  317. activeAdmins++
  318. }
  319. }
  320. if activeAdmins <= 1 {
  321. return ErrCannotDeleteAdmin
  322. }
  323. }
  324. delete(m.users, username)
  325. // Invalidate any active sessions of this user
  326. for _, token := range m.userSession[username] {
  327. delete(m.sessions, token)
  328. }
  329. delete(m.userSession, username)
  330. return m.saveLocked()
  331. }
  332. func (m *Manager) ChangePassword(username, oldPassword, newPassword string, isAdmin bool) error {
  333. if len(newPassword) < 4 {
  334. return ErrPasswordTooShort
  335. }
  336. m.mu.Lock()
  337. defer m.mu.Unlock()
  338. u, exists := m.users[username]
  339. if !exists {
  340. return ErrUserNotFound
  341. }
  342. if !isAdmin {
  343. if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(oldPassword)); err != nil {
  344. return errors.New("current password incorrect")
  345. }
  346. }
  347. hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
  348. if err != nil {
  349. return fmt.Errorf("failed to hash password: %w", err)
  350. }
  351. u.PasswordHash = string(hash)
  352. m.users[username] = u
  353. return m.saveLocked()
  354. }
  355. func (m *Manager) cleanupWorker() {
  356. ticker := time.NewTicker(30 * time.Minute)
  357. defer ticker.Stop()
  358. for range ticker.C {
  359. m.mu.Lock()
  360. now := time.Now()
  361. for token, sess := range m.sessions {
  362. if now.After(sess.ExpiresAt) {
  363. delete(m.sessions, token)
  364. }
  365. }
  366. m.mu.Unlock()
  367. }
  368. }