manager_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. package appproxy
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/http"
  6. "sort"
  7. "sync"
  8. "testing"
  9. )
  10. // memStore is an in-memory Store for tests
  11. type memStore struct {
  12. mu sync.Mutex
  13. tables map[string]map[string][]byte
  14. }
  15. func newMemStore() *memStore {
  16. return &memStore{tables: map[string]map[string][]byte{}}
  17. }
  18. func (s *memStore) NewTable(name string) error {
  19. s.mu.Lock()
  20. defer s.mu.Unlock()
  21. if _, ok := s.tables[name]; !ok {
  22. s.tables[name] = map[string][]byte{}
  23. }
  24. return nil
  25. }
  26. func (s *memStore) Write(table string, key string, value interface{}) error {
  27. s.mu.Lock()
  28. defer s.mu.Unlock()
  29. t, ok := s.tables[table]
  30. if !ok {
  31. return errors.New("no table")
  32. }
  33. js, err := json.Marshal(value)
  34. if err != nil {
  35. return err
  36. }
  37. t[key] = js
  38. return nil
  39. }
  40. func (s *memStore) Delete(table string, key string) error {
  41. s.mu.Lock()
  42. defer s.mu.Unlock()
  43. delete(s.tables[table], key)
  44. return nil
  45. }
  46. func (s *memStore) ListTable(table string) ([][][]byte, error) {
  47. s.mu.Lock()
  48. defer s.mu.Unlock()
  49. keys := []string{}
  50. for k := range s.tables[table] {
  51. keys = append(keys, k)
  52. }
  53. sort.Strings(keys)
  54. rows := [][][]byte{}
  55. for _, k := range keys {
  56. rows = append(rows, [][]byte{[]byte(k), s.tables[table][k]})
  57. }
  58. return rows, nil
  59. }
  60. // testUsers backs ResolveUser / LookupUser in tests. A request picks its user
  61. // with the X-Test-User header
  62. var testUsers = map[string]*Identity{
  63. "admin": {Username: "admin", Groups: []string{"administrator"}, IsAdmin: true},
  64. "alice": {Username: "alice", Groups: []string{"users"}},
  65. "bob": {Username: "bob", Groups: []string{"guests"}},
  66. }
  67. func newTestManager(t *testing.T, store *memStore) *Manager {
  68. t.Helper()
  69. if store == nil {
  70. store = newMemStore()
  71. }
  72. m, err := NewManager(Options{
  73. Store: store,
  74. SelfPort: 8080,
  75. ResolveUser: func(w http.ResponseWriter, r *http.Request) *Identity {
  76. return testUsers[r.Header.Get("X-Test-User")]
  77. },
  78. LookupUser: func(username string) *Identity {
  79. return testUsers[username]
  80. },
  81. LoginURL: func(returnTo string) string {
  82. return "/login.html?redirect=" + returnTo
  83. },
  84. StripCookies: []string{"ao_auth", "ao_acc"},
  85. })
  86. if err != nil {
  87. t.Fatalf("NewManager: %v", err)
  88. }
  89. return m
  90. }
  91. func pathEndpoint(slug string, target string) Endpoint {
  92. return Endpoint{
  93. Slug: slug,
  94. Name: slug,
  95. Target: target,
  96. Mode: ModePath,
  97. Trusted: true,
  98. RootRedirect: true,
  99. RequireLogin: true,
  100. }
  101. }
  102. func TestManagerSaveAndReload(t *testing.T) {
  103. store := newMemStore()
  104. m := newTestManager(t, store)
  105. if _, err := m.Save(pathEndpoint("grafana", "127.0.0.1:3000"), true, "admin", "nas.local"); err != nil {
  106. t.Fatalf("create: %v", err)
  107. }
  108. if _, err := m.Save(pathEndpoint("grafana", "127.0.0.1:3000"), true, "admin", "nas.local"); err == nil {
  109. t.Errorf("creating a duplicate slug should fail")
  110. }
  111. if _, err := m.Save(pathEndpoint("missing", "127.0.0.1:3000"), false, "admin", "nas.local"); err == nil {
  112. t.Errorf("updating a missing app should fail")
  113. }
  114. updated := pathEndpoint("grafana", "127.0.0.1:3001")
  115. saved, err := m.Save(updated, false, "someone-else", "nas.local")
  116. if err != nil {
  117. t.Fatalf("update: %v", err)
  118. }
  119. if saved.CreatedBy != "admin" {
  120. t.Errorf("CreatedBy = %q, want it kept as admin", saved.CreatedBy)
  121. }
  122. reloaded := newTestManager(t, store)
  123. ep, ok := reloaded.Get("grafana")
  124. if !ok {
  125. t.Fatalf("endpoint not reloaded from the store")
  126. }
  127. if ep.Target != "127.0.0.1:3001" {
  128. t.Errorf("reloaded target = %q", ep.Target)
  129. }
  130. if err := reloaded.Delete("grafana"); err != nil {
  131. t.Fatalf("delete: %v", err)
  132. }
  133. if reloaded.Count() != 0 {
  134. t.Errorf("Count after delete = %d", reloaded.Count())
  135. }
  136. if rows, _ := store.ListTable(tableName); len(rows) != 0 {
  137. t.Errorf("store still has %d rows", len(rows))
  138. }
  139. if err := reloaded.Delete("grafana"); err == nil {
  140. t.Errorf("deleting twice should fail")
  141. }
  142. }
  143. func TestManagerHostnameRules(t *testing.T) {
  144. m := newTestManager(t, nil)
  145. sub := func(slug string, host string) Endpoint {
  146. return Endpoint{Slug: slug, Target: "127.0.0.1:9000", Mode: ModeSubdomain, Hostname: host, PortalOrigin: "https://nas.example.com", RequireLogin: true}
  147. }
  148. tests := []struct {
  149. name string
  150. ep Endpoint
  151. create bool
  152. host string
  153. wantErr bool
  154. }{
  155. {"first app", sub("one", "one.example.com"), true, "nas.example.com", false},
  156. {"hostname taken", sub("two", "one.example.com"), true, "nas.example.com", true},
  157. {"hostname is the portal", sub("three", "nas.example.com"), true, "other.example.com", true},
  158. {"hostname is the admin's host", sub("four", "four.example.com"), true, "four.example.com:8080", true},
  159. {"same app keeps its hostname", sub("one", "one.example.com"), false, "nas.example.com", false},
  160. }
  161. for _, tt := range tests {
  162. t.Run(tt.name, func(t *testing.T) {
  163. _, err := m.Save(tt.ep, tt.create, "admin", tt.host)
  164. if (err != nil) != tt.wantErr {
  165. t.Errorf("Save err = %v, wantErr %v", err, tt.wantErr)
  166. }
  167. })
  168. }
  169. if rt := m.runtimeByHost("ONE.example.com:443"); rt == nil || rt.ep.Slug != "one" {
  170. t.Errorf("host lookup should ignore case and port")
  171. }
  172. }
  173. func TestManagerAccessibleBy(t *testing.T) {
  174. m := newTestManager(t, nil)
  175. open := pathEndpoint("open", "127.0.0.1:1001")
  176. staff := pathEndpoint("staff", "127.0.0.1:1002")
  177. staff.AllowedGroups = []string{"users"}
  178. public := pathEndpoint("public", "127.0.0.1:1003")
  179. public.RequireLogin = false
  180. for _, ep := range []Endpoint{open, staff, public} {
  181. if _, err := m.Save(ep, true, "admin", "nas.local"); err != nil {
  182. t.Fatalf("save %s: %v", ep.Slug, err)
  183. }
  184. }
  185. tests := []struct {
  186. user string
  187. want int
  188. }{
  189. {"admin", 3},
  190. {"alice", 3},
  191. {"bob", 2},
  192. {"", 1},
  193. }
  194. for _, tt := range tests {
  195. t.Run(tt.user, func(t *testing.T) {
  196. if got := len(m.AccessibleBy(testUsers[tt.user])); got != tt.want {
  197. t.Errorf("AccessibleBy(%q) = %d apps, want %d", tt.user, got, tt.want)
  198. }
  199. })
  200. }
  201. }
  202. func TestManagerStatsSurviveEdit(t *testing.T) {
  203. m := newTestManager(t, nil)
  204. if _, err := m.Save(pathEndpoint("app", "127.0.0.1:1001"), true, "admin", "nas.local"); err != nil {
  205. t.Fatal(err)
  206. }
  207. m.runtimeBySlug("app").recordLeak("/x")
  208. if _, err := m.Save(pathEndpoint("app", "127.0.0.1:1002"), false, "admin", "nas.local"); err != nil {
  209. t.Fatal(err)
  210. }
  211. s := m.Stats("app")
  212. if s.Leaks != 1 || len(s.LastLeaks) != 1 || s.LastLeaks[0] != "/x" {
  213. t.Errorf("stats after edit = %+v", s)
  214. }
  215. }