diff options
Diffstat (limited to 'cmd/authproxy/authproxy.go')
| -rw-r--r-- | cmd/authproxy/authproxy.go | 146 |
1 files changed, 146 insertions, 0 deletions
diff --git a/cmd/authproxy/authproxy.go b/cmd/authproxy/authproxy.go new file mode 100644 index 000000000..ec7aa0b91 --- /dev/null +++ b/cmd/authproxy/authproxy.go @@ -0,0 +1,146 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os/exec" + "strings" + "sync" + "time" + + grafanaclient "github.com/nytm/go-grafana-api" + "inet.af/netaddr" + "tailscale.com/ipn/ipnstate" + "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" +) + +var spoofAdmin = flag.Bool("spoof-admin", false, "make everybody be an admin") + +func main() { + flag.Parse() + log.Printf("starting") + ln, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatal(err) + } + log.Printf("listening on %v", ln.Addr()) + target, _ := url.Parse("http://localhost:80") + rp := httputil.NewSingleHostReverseProxy(target) + + creds, err := ioutil.ReadFile("/etc/grafana/admin-creds.authproxy") + if err != nil { + log.Fatal(err) + } + userColonPass := strings.TrimSpace(string(creds)) + log.Printf("user pass: %q", userColonPass) + + gc, err := grafanaclient.New(userColonPass, "http://localhost") + if err != nil { + log.Fatal(err) + } + + var ( + addMu sync.Mutex + added = map[string]bool{} + ) + addUser := func(email, role string) { + addMu.Lock() + defer addMu.Unlock() + if added[email] { + return + } + added[email] = true + err := gc.AddOrgUser(1, "email", role) + log.Printf("adding org user %s as %v: %v", email, role, err) + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + ipp, err := netaddr.ParseIPPort(r.RemoteAddr) + if err != nil { + http.Error(w, "bad RemoteAddr", 400) + return + } + if !tsaddr.IsTailscaleIP(ipp.IP) { + http.Error(w, "not a Tailscale IP", 403) + return + } + tstat, err := getTailscaleStatus() + if err != nil { + log.Printf("getting Tailscale status: %v", err) + http.Error(w, "failed to get Tailscale status", 500) + return + } + ro := r.Clone(r.Context()) + if u, ok := tstat.userOfIP(ipp.IP); ok && !strings.HasPrefix(r.RequestURI, "/invite") { + role := "viewer" + if strings.HasSuffix(u.LoginName, "@tailscale.com") { + role = "editor" + } + email := strings.Replace(u.LoginName, "@", "-auto@", 1) + addUser(email, role) + log.Printf("serving %v, %v, %v", email, r.RemoteAddr, r.RequestURI) + ro.Header.Add("X-Webauth-User", email) + ro.Header.Add("X-User-Name", u.DisplayName) + ro.Header.Add("X-User-Email", email) + } else { + log.Printf("serving ??, %v, %v", r.RemoteAddr, r.RequestURI) + } + if *spoofAdmin { + ro.Header.Add("X-Webauth-User", "apenwarr@tailscale.com") + } + rp.ServeHTTP(w, ro) + }) + var hs http.Server + log.Fatal(hs.Serve(ln)) +} + +// /etc/grafana/admin-creds.authproxy +// curl -v -X PATCH -u 'apenwarr@tailscale.com:XXXXX' --data '{"role":"Editor"}' -H "Content-Type:application/json" http://localhost:80/api/org/users/ + +var ( + mu sync.Mutex + tsCache *tailscaleStatus +) + +func getTailscaleStatus() (*tailscaleStatus, error) { + mu.Lock() + defer mu.Unlock() + if s := tsCache; s != nil && time.Since(s.at) < 10*time.Second { + return s, nil + } + out, err := exec.Command("tailscale", "status", "--json").Output() + if err != nil { + return nil, err + } + tss := &tailscaleStatus{at: time.Now()} + if err := json.Unmarshal(out, &tss.s); err != nil { + return nil, err + } + if tss.s.BackendState != "Running" { + return nil, fmt.Errorf("tailscale not running; in state %q", tss.s.BackendState) + } + return tss, nil +} + +type tailscaleStatus struct { + at time.Time + s ipnstate.Status +} + +func (tss *tailscaleStatus) userOfIP(ip netaddr.IP) (u tailcfg.UserProfile, ok bool) { + for _, ps := range tss.s.Peer { + if peerIP, err := netaddr.ParseIP(ps.TailAddr); err == nil && ip == peerIP { + u, ok = tss.s.User[ps.UserID] + return + } + } + return u, false +} |
