summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2021-01-04 08:41:10 -0800
committerBrad Fitzpatrick <bradfitz@tailscale.com>2021-01-04 08:41:10 -0800
commitf2dc6433602f9ddd3d6262770c899a863c9a098c (patch)
treeb781f5d8f60c8acbbfbcb35f40911fe73572f964
parent8fc11d582dcc25f458a4d11bc3bfecffd6855351 (diff)
downloadtailscale-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.go146
-rw-r--r--go.mod1
-rw-r--r--go.sum5
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
+}
diff --git a/go.mod b/go.mod
index 613c21719..bf8054aa0 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index fcc2ccdd4..7f3821013 100644
--- a/go.sum
+++ b/go.sum
@@ -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=