package store import ( "context" "database/sql" "errors" "fmt" "strings" "time" ) // Roles, from the most to the least privileged. const ( RoleAdmin = "admin" // everything, including clusters, settings and users RoleOperator = "operator" // dry-run and synchronization RoleViewer = "viewer" // read only ) // ValidRole tells whether r is a known role. func ValidRole(r string) bool { return r == RoleAdmin || r == RoleOperator || r == RoleViewer } // RoleRank orders the roles: admin 3, operator 2, viewer 1, unknown 0. func RoleRank(r string) int { switch r { case RoleAdmin: return 3 case RoleOperator: return 2 case RoleViewer: return 1 } return 0 } // User is a local account (password hash never returned). type User struct { Name string `json:"name"` Role string `json:"role"` Epoch int64 `json:"-"` // changes with the password: ends the sessions CreatedAt time.Time `json:"createdAt"` LastLogin time.Time `json:"lastLogin"` hash string } // Users lists the local accounts. func (s *Store) Users(ctx context.Context) ([]User, error) { rows, err := s.db.QueryContext(ctx, `SELECT name, role, epoch, created_at, last_login FROM users ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var out []User for rows.Next() { var u User var c, l int64 if err := rows.Scan(&u.Name, &u.Role, &u.Epoch, &c, &l); err != nil { return nil, err } u.CreatedAt = time.Unix(c, 0).UTC() if l > 0 { u.LastLogin = time.Unix(l, 0).UTC() } out = append(out, u) } return out, rows.Err() } // User returns one account. func (s *Store) User(ctx context.Context, name string) (User, error) { var u User var c, l int64 err := s.db.QueryRowContext(ctx, `SELECT name, role, password, epoch, created_at, last_login FROM users WHERE name = ?`, name). Scan(&u.Name, &u.Role, &u.hash, &u.Epoch, &c, &l) if errors.Is(err, sql.ErrNoRows) { return u, ErrNotFound } u.CreatedAt = time.Unix(c, 0).UTC() if l > 0 { u.LastLogin = time.Unix(l, 0).UTC() } return u, err } // CountUsers returns the number of local accounts. func (s *Store) CountUsers(ctx context.Context) (int, error) { var n int err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM users`).Scan(&n) return n, err } // Authenticate checks a password and records the login. func (s *Store) Authenticate(ctx context.Context, name, pass string) (User, bool) { u, err := s.User(ctx, name) if err != nil { // same cost as a real check: the answer time does not tell whether the user exists CheckPassword(dummyHash, pass) return User{}, false } if !CheckPassword(u.hash, pass) { return User{}, false } _, _ = s.db.ExecContext(ctx, `UPDATE users SET last_login = ? WHERE name = ?`, time.Now().Unix(), name) return u, true } var dummyHash, _ = HashPassword("dummy password") // SaveUser creates an account or changes its role and/or password (empty: unchanged). func (s *Store) SaveUser(ctx context.Context, name, role, pass string) error { name = strings.TrimSpace(name) if name == "" || strings.ContainsAny(name, " \t\r\n") { return errors.New("invalid user name") } if !ValidRole(role) { return fmt.Errorf("unknown role %q", role) } if pass != "" && len(pass) < 8 { return errors.New("password: 8 characters minimum") } var hash string if pass != "" { var err error if hash, err = HashPassword(pass); err != nil { return err } } now := time.Now() _, err := s.User(ctx, name) switch { case errors.Is(err, ErrNotFound): if pass == "" { return errors.New("a password is required for a new user") } _, err = s.db.ExecContext(ctx, `INSERT INTO users (name, role, password, epoch, created_at) VALUES (?,?,?,?,?)`, name, role, hash, now.UnixNano(), now.Unix()) return err case err != nil: return err } if err := s.keepAnAdmin(ctx, name, role); err != nil { return err } if pass == "" { _, err = s.db.ExecContext(ctx, `UPDATE users SET role = ? WHERE name = ?`, role, name) } else { _, err = s.db.ExecContext(ctx, `UPDATE users SET role = ?, password = ?, epoch = ? WHERE name = ?`, role, hash, now.UnixNano(), name) } return err } // DeleteUser removes an account (never the last administrator). func (s *Store) DeleteUser(ctx context.Context, name string) error { if err := s.keepAnAdmin(ctx, name, ""); err != nil { return err } res, err := s.db.ExecContext(ctx, `DELETE FROM users WHERE name = ?`, name) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } // keepAnAdmin refuses a change that would leave no local administrator // (OIDC may be down: a local admin is the way back in). func (s *Store) keepAnAdmin(ctx context.Context, name, newRole string) error { if newRole == RoleAdmin { return nil } var n int if err := s.db.QueryRowContext(ctx, `SELECT count(*) FROM users WHERE role = 'admin' AND name <> ?`, name).Scan(&n); err != nil { return err } var cur string _ = s.db.QueryRowContext(ctx, `SELECT role FROM users WHERE name = ?`, name).Scan(&cur) if cur == RoleAdmin && n == 0 { return errors.New("this is the last local administrator") } return nil } // ErrSetupDone: the first administrator already exists. var ErrSetupDone = errors.New("the first administrator already exists") // CreateFirstAdmin creates the first administrator from the web page shown on an // empty database. The insert only happens while the table is empty: two // concurrent requests cannot both succeed. func (s *Store) CreateFirstAdmin(ctx context.Context, name, pass string) error { name = strings.TrimSpace(name) if name == "" || strings.ContainsAny(name, " \t\r\n") { return errors.New("invalid user name") } if len(pass) < 12 { return errors.New("password: 12 characters minimum") } hash, err := HashPassword(pass) if err != nil { return err } now := time.Now() res, err := s.db.ExecContext(ctx, `INSERT INTO users (name, role, password, epoch, created_at) SELECT ?, 'admin', ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM users)`, name, hash, now.UnixNano(), now.Unix()) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrSetupDone } return nil }