diff options
| author | Brad Fitzpatrick <bradfitz@tailscale.com> | 2021-01-04 08:41:10 -0800 |
|---|---|---|
| committer | Brad Fitzpatrick <bradfitz@tailscale.com> | 2021-01-04 08:41:10 -0800 |
| commit | f2dc6433602f9ddd3d6262770c899a863c9a098c (patch) | |
| tree | b781f5d8f60c8acbbfbcb35f40911fe73572f964 | |
| parent | 8fc11d582dcc25f458a4d11bc3bfecffd6855351 (diff) | |
| download | tailscale-bradfitz/grafana_auth_proxy.tar.xz tailscale-bradfitz/grafana_auth_proxy.zip | |
WIP: grafana auth proxybradfitz/grafana_auth_proxy
| -rw-r--r-- | cmd/authproxy/authproxy.go | 146 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 5 |
3 files changed, 152 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 +} @@ -21,6 +21,7 @@ require ( github.com/mdlayher/netlink v1.2.0 github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a github.com/miekg/dns v1.1.30 + github.com/nytm/go-grafana-api v0.5.0 github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 github.com/peterbourgon/ff/v2 v2.0.0 github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 @@ -32,6 +32,7 @@ github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY= github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= +github.com/gobs/pretty v0.0.0-20180724170744-09732c25a95b/go.mod h1:Xo4aNUOrJnVruqWQJBtW6+bTBDTniY8yZum5rF3b5jw= github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= @@ -52,6 +53,8 @@ github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbC github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w= github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= @@ -86,6 +89,8 @@ github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jq github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4= github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo= github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/nytm/go-grafana-api v0.5.0 h1:8pIbNPNDguBa4aUNxcYl0GN247W6PXMvsOwiRBmk1sE= +github.com/nytm/go-grafana-api v0.5.0/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA= |
