-
Notifications
You must be signed in to change notification settings - Fork 79
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add support for golink peer capability
The "tailscale.com/golink" peercap includes a single "admin" bool field. When set, this grants the user the ability to edit all links stored in the system. Update currentUser to return a simple user struct instead of just a bare username. Rename checkLinkOwnership to canEditLink and change to a bool return value. Signed-off-by: Will Norris <[email protected]>
- Loading branch information
1 parent
1f9fe17
commit fd97618
Showing
2 changed files
with
81 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,6 @@ import ( | |
"fmt" | ||
"html/template" | ||
"io/fs" | ||
"io/ioutil" | ||
"log" | ||
"net" | ||
"net/http" | ||
|
@@ -35,6 +34,7 @@ import ( | |
"tailscale.com/client/tailscale" | ||
"tailscale.com/hostinfo" | ||
"tailscale.com/ipn" | ||
"tailscale.com/tailcfg" | ||
"tailscale.com/tsnet" | ||
) | ||
|
||
|
@@ -88,7 +88,7 @@ func Run() error { | |
|
||
if *sqlitefile == "" { | ||
if devMode() { | ||
tmpdir, err := ioutil.TempDir("", "golink_dev_*") | ||
tmpdir, err := os.MkdirTemp("", "golink_dev_*") | ||
if err != nil { | ||
return err | ||
} | ||
|
@@ -396,8 +396,8 @@ func serveGo(w http.ResponseWriter, r *http.Request) { | |
stats.dirty[link.Short]++ | ||
stats.mu.Unlock() | ||
|
||
login, _ := currentUser(r) | ||
env := expandEnv{Now: time.Now().UTC(), Path: remainder, user: login, query: r.URL.Query()} | ||
cu, _ := currentUser(r) | ||
env := expandEnv{Now: time.Now().UTC(), Path: remainder, user: cu.login, query: r.URL.Query()} | ||
target, err := expandLink(link.Long, env) | ||
if err != nil { | ||
log.Printf("expanding %q: %v", link.Long, err) | ||
|
@@ -446,21 +446,24 @@ func serveDetail(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
|
||
login, err := currentUser(r) | ||
cu, err := currentUser(r) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
canEdit := canEditLink(r.Context(), link, cu) | ||
ownerExists, err := userExists(r.Context(), link.Owner) | ||
if err != nil { | ||
log.Printf("looking up tailnet user %q: %v", link.Owner, err) | ||
} | ||
|
||
data := detailData{Link: link} | ||
if link.Owner == login || !ownerExists { | ||
data.Editable = true | ||
data.Link.Owner = login | ||
data.XSRF = xsrftoken.Generate(xsrfKey, login, short) | ||
data := detailData{ | ||
Link: link, | ||
Editable: canEdit, | ||
XSRF: xsrftoken.Generate(xsrfKey, cu.login, short), | ||
} | ||
if canEdit && !ownerExists { | ||
data.Link.Owner = cu.login | ||
} | ||
|
||
detailTmpl.Execute(w, data) | ||
|
@@ -541,24 +544,42 @@ func expandLink(long string, env expandEnv) (*url.URL, error) { | |
|
||
func devMode() bool { return *dev != "" } | ||
|
||
const peerCapName = "tailscale.com/golink" | ||
|
||
type capabilities struct { | ||
Admin bool `json:"admin"` | ||
} | ||
|
||
type user struct { | ||
login string | ||
isAdmin bool | ||
} | ||
|
||
// currentUser returns the Tailscale user associated with the request. | ||
// In most cases, this will be the user that owns the device that made the request. | ||
// For tagged devices, the value "tagged-devices" is returned. | ||
// If the user can't be determined (such as requests coming through a subnet router), | ||
// an error is returned unless the -allow-unknown-users flag is set. | ||
var currentUser = func(r *http.Request) (string, error) { | ||
var currentUser = func(r *http.Request) (user, error) { | ||
if devMode() { | ||
return "[email protected]", nil | ||
return user{login: "[email protected]"}, nil | ||
} | ||
whois, err := localClient.WhoIs(r.Context(), r.RemoteAddr) | ||
if err != nil { | ||
if *allowUnknownUsers { | ||
// Don't report the error if we are allowing unknown users. | ||
return "", nil | ||
return user{}, nil | ||
} | ||
return "", err | ||
return user{}, err | ||
} | ||
return whois.UserProfile.LoginName, nil | ||
login := whois.UserProfile.LoginName | ||
caps, _ := tailcfg.UnmarshalCapJSON[capabilities](whois.CapMap, peerCapName) | ||
for _, cap := range caps { | ||
if cap.Admin { | ||
return user{login: login, isAdmin: true}, nil | ||
} | ||
} | ||
return user{login: login}, nil | ||
} | ||
|
||
// userExists returns whether a user exists with the specified login in the current tailnet. | ||
|
@@ -597,7 +618,7 @@ func serveDelete(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
|
||
login, err := currentUser(r) | ||
cu, err := currentUser(r) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
|
@@ -609,12 +630,12 @@ func serveDelete(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
|
||
if err := checkLinkOwnership(r.Context(), link, login); err != nil { | ||
http.Error(w, fmt.Sprintf("cannot delete link: %v", err), http.StatusForbidden) | ||
if !canEditLink(r.Context(), link, cu) { | ||
http.Error(w, fmt.Sprintf("cannot delete link owned by %q", link.Owner), http.StatusForbidden) | ||
return | ||
} | ||
|
||
if !xsrftoken.Valid(r.PostFormValue("xsrf"), xsrfKey, login, short) { | ||
if !xsrftoken.Valid(r.PostFormValue("xsrf"), xsrfKey, cu.login, short) { | ||
http.Error(w, "invalid XSRF token", http.StatusBadRequest) | ||
return | ||
} | ||
|
@@ -646,7 +667,7 @@ func serveSave(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
|
||
login, err := currentUser(r) | ||
cu, err := currentUser(r) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
|
@@ -657,8 +678,8 @@ func serveSave(w http.ResponseWriter, r *http.Request) { | |
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
} | ||
|
||
if err := checkLinkOwnership(r.Context(), link, login); err != nil { | ||
http.Error(w, fmt.Sprintf("cannot update link: %v", err), http.StatusForbidden) | ||
if !canEditLink(r.Context(), link, cu) { | ||
http.Error(w, fmt.Sprintf("cannot update link owned by %q", link.Owner), http.StatusForbidden) | ||
return | ||
} | ||
|
||
|
@@ -674,7 +695,7 @@ func serveSave(w http.ResponseWriter, r *http.Request) { | |
return | ||
} | ||
} else { | ||
owner = login | ||
owner = cu.login | ||
} | ||
|
||
now := time.Now().UTC() | ||
|
@@ -701,21 +722,25 @@ func serveSave(w http.ResponseWriter, r *http.Request) { | |
} | ||
} | ||
|
||
func checkLinkOwnership(ctx context.Context, link *Link, login string) error { | ||
// canEditLink returns whether the specified user has permission to edit link. | ||
// Admin users can edit all links. | ||
// Non-admin users can only edit their own links or links without an active owner. | ||
func canEditLink(ctx context.Context, link *Link, u user) bool { | ||
if link == nil || link.Owner == "" { | ||
return nil | ||
// new or unowned link | ||
return true | ||
} | ||
|
||
if u.isAdmin || link.Owner == u.login { | ||
return true | ||
} | ||
|
||
linkOwnerExists, err := userExists(ctx, link.Owner) | ||
owned, err := userExists(ctx, link.Owner) | ||
if err != nil { | ||
log.Printf("looking up tailnet user %q: %v", link.Owner, err) | ||
} | ||
// Don't allow deleting or updating links if the owner account still exists | ||
// or if we're unsure because an error occurred. | ||
if (linkOwnerExists && link.Owner != login) || err != nil { | ||
return fmt.Errorf("link owned by user %q", link.Owner) | ||
} | ||
return nil | ||
// Allow editing if the link is currently unowned | ||
return err == nil && !owned | ||
} | ||
|
||
// serveExport prints a snapshot of the link database. Links are JSON encoded | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,7 +34,7 @@ func TestServeGo(t *testing.T) { | |
tests := []struct { | ||
name string | ||
link string | ||
currentUser func(*http.Request) (string, error) | ||
currentUser func(*http.Request) (user, error) | ||
wantStatus int | ||
wantLink string | ||
}{ | ||
|
@@ -47,7 +47,7 @@ func TestServeGo(t *testing.T) { | |
{ | ||
name: "simple link, anonymous request", | ||
link: "/who", | ||
currentUser: func(*http.Request) (string, error) { return "", nil }, | ||
currentUser: func(*http.Request) (user, error) { return user{}, nil }, | ||
wantStatus: http.StatusFound, | ||
wantLink: "http://who/", | ||
}, | ||
|
@@ -88,7 +88,7 @@ func TestServeGo(t *testing.T) { | |
{ | ||
name: "user link, anonymous request", | ||
link: "/me", | ||
currentUser: func(*http.Request) (string, error) { return "", nil }, | ||
currentUser: func(*http.Request) (user, error) { return user{}, nil }, | ||
wantStatus: http.StatusUnauthorized, | ||
}, | ||
} | ||
|
@@ -131,7 +131,7 @@ func TestServeSave(t *testing.T) { | |
short string | ||
long string | ||
allowUnknownUsers bool | ||
currentUser func(*http.Request) (string, error) | ||
currentUser func(*http.Request) (user, error) | ||
wantStatus int | ||
}{ | ||
{ | ||
|
@@ -156,29 +156,36 @@ func TestServeSave(t *testing.T) { | |
name: "disallow editing another's link", | ||
short: "who", | ||
long: "http://who/", | ||
currentUser: func(*http.Request) (string, error) { return "[email protected]", nil }, | ||
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]"}, nil }, | ||
wantStatus: http.StatusForbidden, | ||
}, | ||
{ | ||
name: "allow editing link owned by tagged-devices", | ||
short: "link-owned-by-tagged-devices", | ||
long: "/after", | ||
currentUser: func(*http.Request) (string, error) { return "[email protected]", nil }, | ||
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]"}, nil }, | ||
wantStatus: http.StatusOK, | ||
}, | ||
{ | ||
name: "admins can edit any link", | ||
short: "who", | ||
long: "http://who/", | ||
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]", isAdmin: true}, nil }, | ||
wantStatus: http.StatusOK, | ||
}, | ||
{ | ||
name: "disallow unknown users", | ||
short: "who2", | ||
long: "http://who/", | ||
currentUser: func(*http.Request) (string, error) { return "", errors.New("") }, | ||
currentUser: func(*http.Request) (user, error) { return user{}, errors.New("") }, | ||
wantStatus: http.StatusInternalServerError, | ||
}, | ||
{ | ||
name: "allow unknown users", | ||
short: "who2", | ||
long: "http://who/", | ||
allowUnknownUsers: true, | ||
currentUser: func(*http.Request) (string, error) { return "", nil }, | ||
currentUser: func(*http.Request) (user, error) { return user{}, nil }, | ||
wantStatus: http.StatusOK, | ||
}, | ||
} | ||
|
@@ -230,7 +237,7 @@ func TestServeDelete(t *testing.T) { | |
name string | ||
short string | ||
xsrf string | ||
currentUser func(*http.Request) (string, error) | ||
currentUser func(*http.Request) (user, error) | ||
wantStatus int | ||
}{ | ||
{ | ||
|
@@ -254,6 +261,13 @@ func TestServeDelete(t *testing.T) { | |
xsrf: xsrf("link-owned-by-tagged-devices"), | ||
wantStatus: http.StatusOK, | ||
}, | ||
{ | ||
name: "admin can delete unowned link", | ||
short: "a", | ||
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]", isAdmin: true}, nil }, | ||
xsrf: xsrf("a"), | ||
wantStatus: http.StatusOK, | ||
}, | ||
{ | ||
name: "invalid xsrf", | ||
short: "foo", | ||
|
@@ -284,7 +298,7 @@ func TestServeDelete(t *testing.T) { | |
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") | ||
w := httptest.NewRecorder() | ||
serveDelete(w, r) | ||
|
||
t.Logf("response body: %v", w.Body.String()) | ||
if w.Code != tt.wantStatus { | ||
t.Errorf("serveDelete(%q) = %d; want %d", tt.short, w.Code, tt.wantStatus) | ||
} | ||
|