Files
Mike Jensen c9f5d175b5 ipn/localapi: require local admin for dev-set-state-store (#21222)
State keys written by dev-set-state-store are otherwise gated by their own handlers, for example serve-config requires a local admin before storing a _serve/<profile-id> key.

Writing such a key directly through dev-set-state-store skipped that check. Require`IsLocalAdmin` in the handler, the same check serve-config performs.

Credit to @johnnymiranda for reporting this issue.

Fixes tailscale/corp#47886

Change-Id: Ie82f961b016f78895793d801929a4fa11ebf7fd8

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-11 14:07:53 -06:00

76 lines
1.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debug
package localapi
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/tstest"
)
// TestServeDevSetStateStore verifies writing state keys requires a local admin,
// not just PermitWrite; guards against bypassing serve-config's authz check.
func TestServeDevSetStateStore(t *testing.T) {
tstest.Replace(t, &validLocalHostForTesting, true)
profileID := ipn.ProfileID("test-profile")
tests := []struct {
desc string
permitWrite bool
localAdmin bool
wantStatus int
}{
{
desc: "no-permission",
permitWrite: false,
localAdmin: false,
wantStatus: http.StatusForbidden,
},
{
desc: "write-not-admin",
permitWrite: true,
localAdmin: false,
wantStatus: http.StatusUnauthorized,
},
{
desc: "write-admin",
permitWrite: true,
localAdmin: true,
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
h := handlerForTest(t, &Handler{
PermitWrite: tt.permitWrite,
Actor: &ipnauth.TestActor{LocalAdmin: tt.localAdmin},
b: newTestLocalBackend(t),
})
s := httptest.NewServer(h)
t.Cleanup(s.Close)
form := url.Values{
"key": {string(ipn.ServeConfigKey(profileID))},
"value": {"{}"},
}
res, err := s.Client().PostForm(s.URL+"/localapi/v0/dev-set-state-store", form)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != tt.wantStatus {
t.Errorf("res.StatusCode = %d, want %d", res.StatusCode, tt.wantStatus)
}
})
}
}