proxy_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package apiproxy
  5. import (
  6. "net/http"
  7. "net/netip"
  8. "reflect"
  9. "testing"
  10. "github.com/google/go-cmp/cmp"
  11. "go.uber.org/zap"
  12. "tailscale.com/client/tailscale/apitype"
  13. "tailscale.com/tailcfg"
  14. "tailscale.com/util/must"
  15. )
  16. func TestImpersonationHeaders(t *testing.T) {
  17. zl, err := zap.NewDevelopment()
  18. if err != nil {
  19. t.Fatal(err)
  20. }
  21. tests := []struct {
  22. name string
  23. emailish string
  24. tags []string
  25. capMap tailcfg.PeerCapMap
  26. wantHeaders http.Header
  27. }{
  28. {
  29. name: "user",
  30. emailish: "[email protected]",
  31. wantHeaders: http.Header{
  32. "Impersonate-User": {"[email protected]"},
  33. },
  34. },
  35. {
  36. name: "tagged",
  37. emailish: "tagged-device",
  38. tags: []string{"tag:foo", "tag:bar"},
  39. wantHeaders: http.Header{
  40. "Impersonate-User": {"node.ts.net"},
  41. "Impersonate-Group": {"tag:foo", "tag:bar"},
  42. },
  43. },
  44. {
  45. name: "user-with-cap",
  46. emailish: "[email protected]",
  47. capMap: tailcfg.PeerCapMap{
  48. tailcfg.PeerCapabilityKubernetes: {
  49. tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`),
  50. tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated.
  51. tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`),
  52. tailcfg.RawMessage(`{"impersonate":{"groups":["group2"]}}`), // duplicate
  53. // These should be ignored, but should parse correctly.
  54. tailcfg.RawMessage(`{}`),
  55. tailcfg.RawMessage(`{"impersonate":{}}`),
  56. tailcfg.RawMessage(`{"impersonate":{"groups":[]}}`),
  57. },
  58. },
  59. wantHeaders: http.Header{
  60. "Impersonate-Group": {"group1", "group2", "group3", "group4"},
  61. "Impersonate-User": {"[email protected]"},
  62. },
  63. },
  64. {
  65. name: "tagged-with-cap",
  66. emailish: "tagged-device",
  67. tags: []string{"tag:foo", "tag:bar"},
  68. capMap: tailcfg.PeerCapMap{
  69. tailcfg.PeerCapabilityKubernetes: {
  70. tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`),
  71. },
  72. },
  73. wantHeaders: http.Header{
  74. "Impersonate-Group": {"group1"},
  75. "Impersonate-User": {"node.ts.net"},
  76. },
  77. },
  78. {
  79. name: "mix-of-caps",
  80. emailish: "tagged-device",
  81. tags: []string{"tag:foo", "tag:bar"},
  82. capMap: tailcfg.PeerCapMap{
  83. tailcfg.PeerCapabilityKubernetes: {
  84. tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]},"recorder":["tag:foo"],"enforceRecorder":true}`),
  85. },
  86. },
  87. wantHeaders: http.Header{
  88. "Impersonate-Group": {"group1"},
  89. "Impersonate-User": {"node.ts.net"},
  90. },
  91. },
  92. {
  93. name: "bad-cap",
  94. emailish: "tagged-device",
  95. tags: []string{"tag:foo", "tag:bar"},
  96. capMap: tailcfg.PeerCapMap{
  97. tailcfg.PeerCapabilityKubernetes: {
  98. tailcfg.RawMessage(`[]`),
  99. },
  100. },
  101. wantHeaders: http.Header{},
  102. },
  103. }
  104. for _, tc := range tests {
  105. r := must.Get(http.NewRequest("GET", "https://op.ts.net/api/foo", nil))
  106. r = r.WithContext(whoIsKey.WithValue(r.Context(), &apitype.WhoIsResponse{
  107. Node: &tailcfg.Node{
  108. Name: "node.ts.net",
  109. Tags: tc.tags,
  110. },
  111. UserProfile: &tailcfg.UserProfile{
  112. LoginName: tc.emailish,
  113. },
  114. CapMap: tc.capMap,
  115. }))
  116. addImpersonationHeaders(r, zl.Sugar())
  117. if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" {
  118. t.Errorf("unexpected header (-want +got):\n%s", d)
  119. }
  120. }
  121. }
  122. func Test_determineRecorderConfig(t *testing.T) {
  123. addr1, addr2 := netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"), netip.MustParseAddrPort("100.99.99.99:80")
  124. tests := []struct {
  125. name string
  126. wantFailOpen bool
  127. wantRecorderAddresses []netip.AddrPort
  128. who *apitype.WhoIsResponse
  129. }{
  130. {
  131. name: "two_ips_fail_closed",
  132. who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}),
  133. wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
  134. },
  135. {
  136. name: "two_ips_fail_open",
  137. who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}),
  138. wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
  139. wantFailOpen: true,
  140. },
  141. {
  142. name: "odd_rule_combination_fail_closed",
  143. who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}),
  144. wantRecorderAddresses: []netip.AddrPort{addr2, addr1},
  145. },
  146. {
  147. name: "no_caps",
  148. who: whoResp(map[string][]string{}),
  149. wantFailOpen: true,
  150. },
  151. {
  152. name: "no_recorder_caps",
  153. who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(tailcfg.PeerCapabilityKubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}),
  154. wantFailOpen: true,
  155. },
  156. }
  157. for _, tt := range tests {
  158. t.Run(tt.name, func(t *testing.T) {
  159. gotFailOpen, gotRecorderAddresses, err := determineRecorderConfig(tt.who)
  160. if err != nil {
  161. t.Fatalf("unexpected error: %v", err)
  162. }
  163. if gotFailOpen != tt.wantFailOpen {
  164. t.Errorf("determineRecorderConfig() gotFailOpen = %v, want %v", gotFailOpen, tt.wantFailOpen)
  165. }
  166. if !reflect.DeepEqual(gotRecorderAddresses, tt.wantRecorderAddresses) {
  167. t.Errorf("determineRecorderConfig() gotRecorderAddresses = %v, want %v", gotRecorderAddresses, tt.wantRecorderAddresses)
  168. }
  169. })
  170. }
  171. }
  172. func whoResp(capMap map[string][]string) *apitype.WhoIsResponse {
  173. resp := &apitype.WhoIsResponse{
  174. CapMap: tailcfg.PeerCapMap{},
  175. }
  176. for cap, rules := range capMap {
  177. resp.CapMap[tailcfg.PeerCapability(cap)] = raw(rules...)
  178. }
  179. return resp
  180. }
  181. func raw(in ...string) []tailcfg.RawMessage {
  182. var out []tailcfg.RawMessage
  183. for _, i := range in {
  184. out = append(out, tailcfg.RawMessage(i))
  185. }
  186. return out
  187. }