auth.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package web
  4. import (
  5. "context"
  6. "crypto/rand"
  7. "encoding/base64"
  8. "errors"
  9. "fmt"
  10. "net/http"
  11. "net/url"
  12. "slices"
  13. "strings"
  14. "time"
  15. "tailscale.com/client/tailscale/apitype"
  16. "tailscale.com/ipn/ipnstate"
  17. "tailscale.com/tailcfg"
  18. )
  19. const (
  20. sessionCookieName = "TS-Web-Session"
  21. sessionCookieExpiry = time.Hour * 24 * 30 // 30 days
  22. )
  23. // browserSession holds data about a user's browser session
  24. // on the full management web client.
  25. type browserSession struct {
  26. // ID is the unique identifier for the session.
  27. // It is passed in the user's "TS-Web-Session" browser cookie.
  28. ID string
  29. SrcNode tailcfg.NodeID
  30. SrcUser tailcfg.UserID
  31. AuthID string // from tailcfg.WebClientAuthResponse
  32. AuthURL string // from tailcfg.WebClientAuthResponse
  33. Created time.Time
  34. Authenticated bool
  35. }
  36. // isAuthorized reports true if the given session is authorized
  37. // to be used by its associated user to access the full management
  38. // web client.
  39. //
  40. // isAuthorized is true only when s.Authenticated is true (i.e.
  41. // the user has authenticated the session) and the session is not
  42. // expired.
  43. // 2023-10-05: Sessions expire by default 30 days after creation.
  44. func (s *browserSession) isAuthorized(now time.Time) bool {
  45. switch {
  46. case s == nil:
  47. return false
  48. case !s.Authenticated:
  49. return false // awaiting auth
  50. case s.isExpired(now):
  51. return false // expired
  52. }
  53. return true
  54. }
  55. // isExpired reports true if s is expired.
  56. // 2023-10-05: Sessions expire by default 30 days after creation.
  57. func (s *browserSession) isExpired(now time.Time) bool {
  58. return !s.Created.IsZero() && now.After(s.expires())
  59. }
  60. // expires reports when the given session expires.
  61. func (s *browserSession) expires() time.Time {
  62. return s.Created.Add(sessionCookieExpiry)
  63. }
  64. var (
  65. errNoSession = errors.New("no-browser-session")
  66. errNotUsingTailscale = errors.New("not-using-tailscale")
  67. errTaggedRemoteSource = errors.New("tagged-remote-source")
  68. errTaggedLocalSource = errors.New("tagged-local-source")
  69. errNotOwner = errors.New("not-owner")
  70. )
  71. // getSession retrieves the browser session associated with the request,
  72. // if one exists.
  73. //
  74. // An error is returned in any of the following cases:
  75. //
  76. // - (errNotUsingTailscale) The request was not made over tailscale.
  77. //
  78. // - (errNoSession) The request does not have a session.
  79. //
  80. // - (errTaggedRemoteSource) The source is remote (another node) and tagged.
  81. // Users must use their own user-owned devices to manage other nodes'
  82. // web clients.
  83. //
  84. // - (errTaggedLocalSource) The source is local (the same node) and tagged.
  85. // Tagged nodes can only be remotely managed, allowing ACLs to dictate
  86. // access to web clients.
  87. //
  88. // - (errNotOwner) The source is not the owner of this client (if the
  89. // client is user-owned). Only the owner is allowed to manage the
  90. // node via the web client.
  91. //
  92. // If no error is returned, the browserSession is always non-nil.
  93. // getTailscaleBrowserSession does not check whether the session has been
  94. // authorized by the user. Callers can use browserSession.isAuthorized.
  95. //
  96. // The WhoIsResponse is always populated, with a non-nil Node and UserProfile,
  97. // unless getTailscaleBrowserSession reports errNotUsingTailscale.
  98. func (s *Server) getSession(r *http.Request) (*browserSession, *apitype.WhoIsResponse, *ipnstate.Status, error) {
  99. whoIs, whoIsErr := s.lc.WhoIs(r.Context(), r.RemoteAddr)
  100. status, statusErr := s.lc.StatusWithoutPeers(r.Context())
  101. switch {
  102. case whoIsErr != nil:
  103. return nil, nil, status, errNotUsingTailscale
  104. case statusErr != nil:
  105. return nil, whoIs, nil, statusErr
  106. case status.Self == nil:
  107. return nil, whoIs, status, errors.New("missing self node in tailscale status")
  108. case whoIs.Node.IsTagged() && whoIs.Node.StableID == status.Self.ID:
  109. return nil, whoIs, status, errTaggedLocalSource
  110. case whoIs.Node.IsTagged():
  111. return nil, whoIs, status, errTaggedRemoteSource
  112. case !status.Self.IsTagged() && status.Self.UserID != whoIs.UserProfile.ID:
  113. return nil, whoIs, status, errNotOwner
  114. }
  115. srcNode := whoIs.Node.ID
  116. srcUser := whoIs.UserProfile.ID
  117. cookie, err := r.Cookie(sessionCookieName)
  118. if errors.Is(err, http.ErrNoCookie) {
  119. return nil, whoIs, status, errNoSession
  120. } else if err != nil {
  121. return nil, whoIs, status, err
  122. }
  123. v, ok := s.browserSessions.Load(cookie.Value)
  124. if !ok {
  125. return nil, whoIs, status, errNoSession
  126. }
  127. session := v.(*browserSession)
  128. if session.SrcNode != srcNode || session.SrcUser != srcUser {
  129. // In this case the browser cookie is associated with another tailscale node.
  130. // Maybe the source browser's machine was logged out and then back in as a different node.
  131. // Return errNoSession because there is no session for this user.
  132. return nil, whoIs, status, errNoSession
  133. } else if session.isExpired(s.timeNow()) {
  134. // Session expired, remove from session map and return errNoSession.
  135. s.browserSessions.Delete(session.ID)
  136. return nil, whoIs, status, errNoSession
  137. }
  138. return session, whoIs, status, nil
  139. }
  140. // newSession creates a new session associated with the given source user/node,
  141. // and stores it back to the session cache. Creating of a new session includes
  142. // generating a new auth URL from the control server.
  143. func (s *Server) newSession(ctx context.Context, src *apitype.WhoIsResponse) (*browserSession, error) {
  144. sid, err := s.newSessionID()
  145. if err != nil {
  146. return nil, err
  147. }
  148. session := &browserSession{
  149. ID: sid,
  150. SrcNode: src.Node.ID,
  151. SrcUser: src.UserProfile.ID,
  152. Created: s.timeNow(),
  153. }
  154. if s.controlSupportsCheckMode(ctx) {
  155. // control supports check mode, so get a new auth URL and return.
  156. a, err := s.newAuthURL(ctx, src.Node.ID)
  157. if err != nil {
  158. return nil, err
  159. }
  160. session.AuthID = a.ID
  161. session.AuthURL = a.URL
  162. } else {
  163. // control does not support check mode, so there is no additional auth we can do.
  164. session.Authenticated = true
  165. }
  166. s.browserSessions.Store(sid, session)
  167. return session, nil
  168. }
  169. // controlSupportsCheckMode returns whether the current control server supports web client check mode, to verify a user's identity.
  170. // We assume that only "tailscale.com" control servers support check mode.
  171. // This allows the web client to be used with non-standard control servers.
  172. // If an error occurs getting the control URL, this method returns true to fail closed.
  173. //
  174. // TODO(juanfont/headscale#1623): adjust or remove this when headscale supports check mode.
  175. func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
  176. prefs, err := s.lc.GetPrefs(ctx)
  177. if err != nil {
  178. return true
  179. }
  180. controlURL, err := url.Parse(prefs.ControlURLOrDefault(s.polc))
  181. if err != nil {
  182. return true
  183. }
  184. return strings.HasSuffix(controlURL.Host, ".tailscale.com")
  185. }
  186. // awaitUserAuth blocks until the given session auth has been completed
  187. // by the user on the control server, then updates the session cache upon
  188. // completion. An error is returned if control auth failed for any reason.
  189. func (s *Server) awaitUserAuth(ctx context.Context, session *browserSession) error {
  190. if session.isAuthorized(s.timeNow()) {
  191. return nil // already authorized
  192. }
  193. a, err := s.waitAuthURL(ctx, session.AuthID, session.SrcNode)
  194. if err != nil {
  195. // Clean up the session. Doing this on any error from control
  196. // server to avoid the user getting stuck with a bad session
  197. // cookie.
  198. s.browserSessions.Delete(session.ID)
  199. return err
  200. }
  201. if a.Complete {
  202. session.Authenticated = a.Complete
  203. s.browserSessions.Store(session.ID, session)
  204. }
  205. return nil
  206. }
  207. func (s *Server) newSessionID() (string, error) {
  208. raw := make([]byte, 16)
  209. for range 5 {
  210. if _, err := rand.Read(raw); err != nil {
  211. return "", err
  212. }
  213. cookie := "ts-web-" + base64.RawURLEncoding.EncodeToString(raw)
  214. if _, ok := s.browserSessions.Load(cookie); !ok {
  215. return cookie, nil
  216. }
  217. }
  218. return "", errors.New("too many collisions generating new session; please refresh page")
  219. }
  220. // peerCapabilities holds information about what a source
  221. // peer is allowed to edit via the web UI.
  222. //
  223. // map value is true if the peer can edit the given feature.
  224. // Only capFeatures included in validCaps will be included.
  225. type peerCapabilities map[capFeature]bool
  226. // canEdit is true if the peerCapabilities grant edit access
  227. // to the given feature.
  228. func (p peerCapabilities) canEdit(feature capFeature) bool {
  229. if p == nil {
  230. return false
  231. }
  232. if p[capFeatureAll] {
  233. return true
  234. }
  235. return p[feature]
  236. }
  237. // isEmpty is true if p is either nil or has no capabilities
  238. // with value true.
  239. func (p peerCapabilities) isEmpty() bool {
  240. if p == nil {
  241. return true
  242. }
  243. for _, v := range p {
  244. if v == true {
  245. return false
  246. }
  247. }
  248. return true
  249. }
  250. type capFeature string
  251. const (
  252. // The following values should not be edited.
  253. // New caps can be added, but existing ones should not be changed,
  254. // as these exact values are used by users in tailnet policy files.
  255. //
  256. // IMPORTANT: When adding a new cap, also update validCaps slice below.
  257. capFeatureAll capFeature = "*" // grants peer management of all features
  258. capFeatureSSH capFeature = "ssh" // grants peer SSH server management
  259. capFeatureSubnets capFeature = "subnets" // grants peer subnet routes management
  260. capFeatureExitNodes capFeature = "exitnodes" // grants peer ability to advertise-as and use exit nodes
  261. capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
  262. )
  263. // validCaps contains the list of valid capabilities used in the web client.
  264. // Any capabilities included in a peer's grants that do not fall into this
  265. // list will be ignored.
  266. var validCaps []capFeature = []capFeature{
  267. capFeatureAll,
  268. capFeatureSSH,
  269. capFeatureSubnets,
  270. capFeatureExitNodes,
  271. capFeatureAccount,
  272. }
  273. type capRule struct {
  274. CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
  275. }
  276. // toPeerCapabilities parses out the web ui capabilities from the
  277. // given whois response.
  278. func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) (peerCapabilities, error) {
  279. if whois == nil || status == nil {
  280. return peerCapabilities{}, nil
  281. }
  282. if whois.Node.IsTagged() {
  283. // We don't allow management *from* tagged nodes, so ignore caps.
  284. // The web client auth flow relies on having a true user identity
  285. // that can be verified through login.
  286. return peerCapabilities{}, nil
  287. }
  288. if !status.Self.IsTagged() {
  289. // User owned nodes are only ever manageable by the owner.
  290. if status.Self.UserID != whois.UserProfile.ID {
  291. return peerCapabilities{}, nil
  292. } else {
  293. return peerCapabilities{capFeatureAll: true}, nil // owner can edit all features
  294. }
  295. }
  296. // For tagged nodes, we actually look at the granted capabilities.
  297. caps := peerCapabilities{}
  298. rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
  299. if err != nil {
  300. return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
  301. }
  302. for _, c := range rules {
  303. for _, f := range c.CanEdit {
  304. cap := capFeature(strings.ToLower(f))
  305. if slices.Contains(validCaps, cap) {
  306. caps[cap] = true
  307. }
  308. }
  309. }
  310. return caps, nil
  311. }