Skip to content

Commit fd97618

Browse files
committed
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]>
1 parent 1f9fe17 commit fd97618

File tree

2 files changed

+81
-42
lines changed

2 files changed

+81
-42
lines changed

golink.go

+57-32
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"fmt"
1818
"html/template"
1919
"io/fs"
20-
"io/ioutil"
2120
"log"
2221
"net"
2322
"net/http"
@@ -35,6 +34,7 @@ import (
3534
"tailscale.com/client/tailscale"
3635
"tailscale.com/hostinfo"
3736
"tailscale.com/ipn"
37+
"tailscale.com/tailcfg"
3838
"tailscale.com/tsnet"
3939
)
4040

@@ -88,7 +88,7 @@ func Run() error {
8888

8989
if *sqlitefile == "" {
9090
if devMode() {
91-
tmpdir, err := ioutil.TempDir("", "golink_dev_*")
91+
tmpdir, err := os.MkdirTemp("", "golink_dev_*")
9292
if err != nil {
9393
return err
9494
}
@@ -396,8 +396,8 @@ func serveGo(w http.ResponseWriter, r *http.Request) {
396396
stats.dirty[link.Short]++
397397
stats.mu.Unlock()
398398

399-
login, _ := currentUser(r)
400-
env := expandEnv{Now: time.Now().UTC(), Path: remainder, user: login, query: r.URL.Query()}
399+
cu, _ := currentUser(r)
400+
env := expandEnv{Now: time.Now().UTC(), Path: remainder, user: cu.login, query: r.URL.Query()}
401401
target, err := expandLink(link.Long, env)
402402
if err != nil {
403403
log.Printf("expanding %q: %v", link.Long, err)
@@ -446,21 +446,24 @@ func serveDetail(w http.ResponseWriter, r *http.Request) {
446446
return
447447
}
448448

449-
login, err := currentUser(r)
449+
cu, err := currentUser(r)
450450
if err != nil {
451451
http.Error(w, err.Error(), http.StatusInternalServerError)
452452
return
453453
}
454+
canEdit := canEditLink(r.Context(), link, cu)
454455
ownerExists, err := userExists(r.Context(), link.Owner)
455456
if err != nil {
456457
log.Printf("looking up tailnet user %q: %v", link.Owner, err)
457458
}
458459

459-
data := detailData{Link: link}
460-
if link.Owner == login || !ownerExists {
461-
data.Editable = true
462-
data.Link.Owner = login
463-
data.XSRF = xsrftoken.Generate(xsrfKey, login, short)
460+
data := detailData{
461+
Link: link,
462+
Editable: canEdit,
463+
XSRF: xsrftoken.Generate(xsrfKey, cu.login, short),
464+
}
465+
if canEdit && !ownerExists {
466+
data.Link.Owner = cu.login
464467
}
465468

466469
detailTmpl.Execute(w, data)
@@ -541,24 +544,42 @@ func expandLink(long string, env expandEnv) (*url.URL, error) {
541544

542545
func devMode() bool { return *dev != "" }
543546

547+
const peerCapName = "tailscale.com/golink"
548+
549+
type capabilities struct {
550+
Admin bool `json:"admin"`
551+
}
552+
553+
type user struct {
554+
login string
555+
isAdmin bool
556+
}
557+
544558
// currentUser returns the Tailscale user associated with the request.
545559
// In most cases, this will be the user that owns the device that made the request.
546560
// For tagged devices, the value "tagged-devices" is returned.
547561
// If the user can't be determined (such as requests coming through a subnet router),
548562
// an error is returned unless the -allow-unknown-users flag is set.
549-
var currentUser = func(r *http.Request) (string, error) {
563+
var currentUser = func(r *http.Request) (user, error) {
550564
if devMode() {
551-
return "[email protected]", nil
565+
return user{login: "[email protected]"}, nil
552566
}
553567
whois, err := localClient.WhoIs(r.Context(), r.RemoteAddr)
554568
if err != nil {
555569
if *allowUnknownUsers {
556570
// Don't report the error if we are allowing unknown users.
557-
return "", nil
571+
return user{}, nil
558572
}
559-
return "", err
573+
return user{}, err
560574
}
561-
return whois.UserProfile.LoginName, nil
575+
login := whois.UserProfile.LoginName
576+
caps, _ := tailcfg.UnmarshalCapJSON[capabilities](whois.CapMap, peerCapName)
577+
for _, cap := range caps {
578+
if cap.Admin {
579+
return user{login: login, isAdmin: true}, nil
580+
}
581+
}
582+
return user{login: login}, nil
562583
}
563584

564585
// 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) {
597618
return
598619
}
599620

600-
login, err := currentUser(r)
621+
cu, err := currentUser(r)
601622
if err != nil {
602623
http.Error(w, err.Error(), http.StatusInternalServerError)
603624
return
@@ -609,12 +630,12 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
609630
return
610631
}
611632

612-
if err := checkLinkOwnership(r.Context(), link, login); err != nil {
613-
http.Error(w, fmt.Sprintf("cannot delete link: %v", err), http.StatusForbidden)
633+
if !canEditLink(r.Context(), link, cu) {
634+
http.Error(w, fmt.Sprintf("cannot delete link owned by %q", link.Owner), http.StatusForbidden)
614635
return
615636
}
616637

617-
if !xsrftoken.Valid(r.PostFormValue("xsrf"), xsrfKey, login, short) {
638+
if !xsrftoken.Valid(r.PostFormValue("xsrf"), xsrfKey, cu.login, short) {
618639
http.Error(w, "invalid XSRF token", http.StatusBadRequest)
619640
return
620641
}
@@ -646,7 +667,7 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
646667
return
647668
}
648669

649-
login, err := currentUser(r)
670+
cu, err := currentUser(r)
650671
if err != nil {
651672
http.Error(w, err.Error(), http.StatusInternalServerError)
652673
return
@@ -657,8 +678,8 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
657678
http.Error(w, err.Error(), http.StatusInternalServerError)
658679
}
659680

660-
if err := checkLinkOwnership(r.Context(), link, login); err != nil {
661-
http.Error(w, fmt.Sprintf("cannot update link: %v", err), http.StatusForbidden)
681+
if !canEditLink(r.Context(), link, cu) {
682+
http.Error(w, fmt.Sprintf("cannot update link owned by %q", link.Owner), http.StatusForbidden)
662683
return
663684
}
664685

@@ -674,7 +695,7 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
674695
return
675696
}
676697
} else {
677-
owner = login
698+
owner = cu.login
678699
}
679700

680701
now := time.Now().UTC()
@@ -701,21 +722,25 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
701722
}
702723
}
703724

704-
func checkLinkOwnership(ctx context.Context, link *Link, login string) error {
725+
// canEditLink returns whether the specified user has permission to edit link.
726+
// Admin users can edit all links.
727+
// Non-admin users can only edit their own links or links without an active owner.
728+
func canEditLink(ctx context.Context, link *Link, u user) bool {
705729
if link == nil || link.Owner == "" {
706-
return nil
730+
// new or unowned link
731+
return true
732+
}
733+
734+
if u.isAdmin || link.Owner == u.login {
735+
return true
707736
}
708737

709-
linkOwnerExists, err := userExists(ctx, link.Owner)
738+
owned, err := userExists(ctx, link.Owner)
710739
if err != nil {
711740
log.Printf("looking up tailnet user %q: %v", link.Owner, err)
712741
}
713-
// Don't allow deleting or updating links if the owner account still exists
714-
// or if we're unsure because an error occurred.
715-
if (linkOwnerExists && link.Owner != login) || err != nil {
716-
return fmt.Errorf("link owned by user %q", link.Owner)
717-
}
718-
return nil
742+
// Allow editing if the link is currently unowned
743+
return err == nil && !owned
719744
}
720745

721746
// serveExport prints a snapshot of the link database. Links are JSON encoded

golink_test.go

+24-10
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestServeGo(t *testing.T) {
3434
tests := []struct {
3535
name string
3636
link string
37-
currentUser func(*http.Request) (string, error)
37+
currentUser func(*http.Request) (user, error)
3838
wantStatus int
3939
wantLink string
4040
}{
@@ -47,7 +47,7 @@ func TestServeGo(t *testing.T) {
4747
{
4848
name: "simple link, anonymous request",
4949
link: "/who",
50-
currentUser: func(*http.Request) (string, error) { return "", nil },
50+
currentUser: func(*http.Request) (user, error) { return user{}, nil },
5151
wantStatus: http.StatusFound,
5252
wantLink: "http://who/",
5353
},
@@ -88,7 +88,7 @@ func TestServeGo(t *testing.T) {
8888
{
8989
name: "user link, anonymous request",
9090
link: "/me",
91-
currentUser: func(*http.Request) (string, error) { return "", nil },
91+
currentUser: func(*http.Request) (user, error) { return user{}, nil },
9292
wantStatus: http.StatusUnauthorized,
9393
},
9494
}
@@ -131,7 +131,7 @@ func TestServeSave(t *testing.T) {
131131
short string
132132
long string
133133
allowUnknownUsers bool
134-
currentUser func(*http.Request) (string, error)
134+
currentUser func(*http.Request) (user, error)
135135
wantStatus int
136136
}{
137137
{
@@ -156,29 +156,36 @@ func TestServeSave(t *testing.T) {
156156
name: "disallow editing another's link",
157157
short: "who",
158158
long: "http://who/",
159-
currentUser: func(*http.Request) (string, error) { return "[email protected]", nil },
159+
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]"}, nil },
160160
wantStatus: http.StatusForbidden,
161161
},
162162
{
163163
name: "allow editing link owned by tagged-devices",
164164
short: "link-owned-by-tagged-devices",
165165
long: "/after",
166-
currentUser: func(*http.Request) (string, error) { return "[email protected]", nil },
166+
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]"}, nil },
167+
wantStatus: http.StatusOK,
168+
},
169+
{
170+
name: "admins can edit any link",
171+
short: "who",
172+
long: "http://who/",
173+
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]", isAdmin: true}, nil },
167174
wantStatus: http.StatusOK,
168175
},
169176
{
170177
name: "disallow unknown users",
171178
short: "who2",
172179
long: "http://who/",
173-
currentUser: func(*http.Request) (string, error) { return "", errors.New("") },
180+
currentUser: func(*http.Request) (user, error) { return user{}, errors.New("") },
174181
wantStatus: http.StatusInternalServerError,
175182
},
176183
{
177184
name: "allow unknown users",
178185
short: "who2",
179186
long: "http://who/",
180187
allowUnknownUsers: true,
181-
currentUser: func(*http.Request) (string, error) { return "", nil },
188+
currentUser: func(*http.Request) (user, error) { return user{}, nil },
182189
wantStatus: http.StatusOK,
183190
},
184191
}
@@ -230,7 +237,7 @@ func TestServeDelete(t *testing.T) {
230237
name string
231238
short string
232239
xsrf string
233-
currentUser func(*http.Request) (string, error)
240+
currentUser func(*http.Request) (user, error)
234241
wantStatus int
235242
}{
236243
{
@@ -254,6 +261,13 @@ func TestServeDelete(t *testing.T) {
254261
xsrf: xsrf("link-owned-by-tagged-devices"),
255262
wantStatus: http.StatusOK,
256263
},
264+
{
265+
name: "admin can delete unowned link",
266+
short: "a",
267+
currentUser: func(*http.Request) (user, error) { return user{login: "[email protected]", isAdmin: true}, nil },
268+
xsrf: xsrf("a"),
269+
wantStatus: http.StatusOK,
270+
},
257271
{
258272
name: "invalid xsrf",
259273
short: "foo",
@@ -284,7 +298,7 @@ func TestServeDelete(t *testing.T) {
284298
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
285299
w := httptest.NewRecorder()
286300
serveDelete(w, r)
287-
301+
t.Logf("response body: %v", w.Body.String())
288302
if w.Code != tt.wantStatus {
289303
t.Errorf("serveDelete(%q) = %d; want %d", tt.short, w.Code, tt.wantStatus)
290304
}

0 commit comments

Comments
 (0)