summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--ipn/ipnlocal/local.go10
-rw-r--r--ipn/ipnlocal/profiles.go23
-rw-r--r--ipn/ipnlocal/profiles_test.go8
-rw-r--r--ipn/localapi/localapi.go8
4 files changed, 46 insertions, 3 deletions
diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go
index 7d1f98b3e..c6de32288 100644
--- a/ipn/ipnlocal/local.go
+++ b/ipn/ipnlocal/local.go
@@ -4204,6 +4204,16 @@ func (b *LocalBackend) DeleteProfile(p ipn.ProfileID) error {
return b.resetForProfileChangeLockedOnEntry()
}
+// DeleteProfiles removes all known profiles.
+func (b *LocalBackend) DeleteAllProfiles() error {
+ b.mu.Lock()
+ if err := b.pm.DeleteAllProfiles(); err != nil {
+ b.mu.Unlock()
+ return err
+ }
+ return b.resetForProfileChangeLockedOnEntry()
+}
+
// CurrentProfile returns the current LoginProfile.
// The value may be zero if the profile is not persisted.
func (b *LocalBackend) CurrentProfile() ipn.LoginProfile {
diff --git a/ipn/ipnlocal/profiles.go b/ipn/ipnlocal/profiles.go
index 5d14c1d94..5c5c832a5 100644
--- a/ipn/ipnlocal/profiles.go
+++ b/ipn/ipnlocal/profiles.go
@@ -12,6 +12,7 @@ import (
"runtime"
"time"
+ "golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
@@ -349,6 +350,21 @@ func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
return pm.writeKnownProfiles()
}
+// DeleteAllProfiles removes all known profiles and switches to a new empty
+// profile.
+func (pm *profileManager) DeleteAllProfiles() error {
+ metricDeleteAllProfile.Add(1)
+
+ for _, kp := range pm.knownProfiles {
+ if err := pm.store.WriteState(kp.Key, nil); err != nil {
+ return err
+ }
+ }
+ pm.NewProfile()
+ maps.Clear(pm.knownProfiles)
+ return pm.writeKnownProfiles()
+}
+
func (pm *profileManager) writeKnownProfiles() error {
b, err := json.Marshal(pm.knownProfiles)
if err != nil {
@@ -512,9 +528,10 @@ func (pm *profileManager) migrateFromLegacyPrefs() error {
}
var (
- metricNewProfile = clientmetric.NewCounter("profiles_new")
- metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
- metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
+ metricNewProfile = clientmetric.NewCounter("profiles_new")
+ metricSwitchProfile = clientmetric.NewCounter("profiles_switch")
+ metricDeleteProfile = clientmetric.NewCounter("profiles_delete")
+ metricDeleteAllProfile = clientmetric.NewCounter("profiles_delete_all")
metricMigration = clientmetric.NewCounter("profiles_migration")
metricMigrationError = clientmetric.NewCounter("profiles_migration_error")
diff --git a/ipn/ipnlocal/profiles_test.go b/ipn/ipnlocal/profiles_test.go
index a7ffee5c2..dc45ed6b5 100644
--- a/ipn/ipnlocal/profiles_test.go
+++ b/ipn/ipnlocal/profiles_test.go
@@ -164,6 +164,14 @@ func TestProfileManagement(t *testing.T) {
delete(wantProfiles, "tagged-node.2.ts.net")
wantCurProfile = "user@2.example.com"
checkProfiles(t)
+
+ t.Logf("Delete all")
+ pm.DeleteAllProfiles()
+ wantProfiles = map[string]ipn.PrefsView{
+ "": emptyPrefs,
+ }
+ wantCurProfile = ""
+ checkProfiles(t)
}
// TestProfileManagementWindows tests going into and out of Unattended mode on
diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go
index 89366aa41..57928d6ba 100644
--- a/ipn/localapi/localapi.go
+++ b/ipn/localapi/localapi.go
@@ -1156,6 +1156,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
// - GET /profiles/: list all profiles (JSON-encoded array of ipn.LoginProfiles)
// - PUT /profiles/: add new profile (no response). A separate
// StartLoginInteractive() is needed to populate and persist the new profile.
+// - DELETE /profiles/: delete all profile (no response)
// - GET /profiles/current: current profile (JSON-ecoded ipn.LoginProfile)
// - GET /profiles/<id>: output profile (JSON-ecoded ipn.LoginProfile)
// - POST /profiles/<id>: switch to profile (no response)
@@ -1182,6 +1183,13 @@ func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
return
}
w.WriteHeader(http.StatusCreated)
+ case http.MethodDelete:
+ err := h.b.DeleteAllProfiles()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusCreated)
default:
http.Error(w, "use GET or PUT", http.StatusMethodNotAllowed)
}