tailscale/cmd/authproxy/authproxy.go
2021-01-04 08:41:10 -08:00

147 lines
3.5 KiB
Go

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
}