Full-access tokens manage users, passwords and tokens
This commit is contained in:
@@ -231,8 +231,9 @@ Base path `/api/v1`. The web interface signs in with a session cookie; every
|
|||||||
user is an admin. Apps and scripts use `Authorization: Bearer <token>`; create
|
user is an admin. Apps and scripts use `Authorization: Bearer <token>`; create
|
||||||
the token under Settings → Pair iOS app. A token belongs to the user who made
|
the token under Settings → Pair iOS app. A token belongs to the user who made
|
||||||
it and is revoked when that user is deleted. A read-only token may only use
|
it and is revoked when that user is deleted. A read-only token may only use
|
||||||
GET. Full-access tokens can do everything the web interface does except the
|
GET. Full-access tokens can do everything the web interface does except backup
|
||||||
endpoints marked "signed in": users, passwords, API tokens, backup and restore.
|
and restore. Users, passwords and API tokens need a full-access token even for
|
||||||
|
reading.
|
||||||
|
|
||||||
`POST /users` and `POST /users/{id}/reset-password` take
|
`POST /users` and `POST /users/{id}/reset-password` take
|
||||||
`{"password": "…", "mustChangePassword": true}`; with `true` (the default) the
|
`{"password": "…", "mustChangePassword": true}`; with `true` (the default) the
|
||||||
@@ -252,7 +253,8 @@ GET /peers/{id}/latency (24 h, one point per 5 minutes)
|
|||||||
GET /peers/{id}/setup (not read-only) DELETE /peers/{id}/setup
|
GET /peers/{id}/setup (not read-only) DELETE /peers/{id}/setup
|
||||||
GET /settings PATCH /settings POST /restart
|
GET /settings PATCH /settings POST /restart
|
||||||
GET /logs?level=&limit=&audit=1 GET /logs/download
|
GET /logs?level=&limit=&audit=1 GET /logs/download
|
||||||
signed in: GET|POST /tokens · DELETE /tokens/{id} · GET /backup · POST /restore
|
GET /tokens POST /tokens DELETE /tokens/{id}
|
||||||
|
signed in: GET /backup · POST /restore
|
||||||
public: GET /setup/{token} · POST /setup/{token} {"pin"} (what a setup link opens)
|
public: GET /setup/{token} · POST /setup/{token} {"pin"} (what a setup link opens)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,17 @@ func (a *App) guard(adminOnly bool, h http.HandlerFunc) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fullAccess refuses read-only tokens, also for GET.
|
||||||
|
func fullAccess(h http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if who(r).Scope == "ro" {
|
||||||
|
writeJSON(w, http.StatusForbidden, map[string]string{"error": "this token is read-only"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// applyResult saves-then-applies: the config is already stored, so a kernel
|
// applyResult saves-then-applies: the config is already stored, so a kernel
|
||||||
// error is reported but does not undo the change.
|
// error is reported but does not undo the change.
|
||||||
func (a *App) apply() string {
|
func (a *App) apply() string {
|
||||||
@@ -105,16 +116,18 @@ func (a *App) routes() http.Handler {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
g := func(pattern string, h http.HandlerFunc) { mux.HandleFunc(pattern, a.guard(false, h)) }
|
g := func(pattern string, h http.HandlerFunc) { mux.HandleFunc(pattern, a.guard(false, h)) }
|
||||||
adm := func(pattern string, h http.HandlerFunc) { mux.HandleFunc(pattern, a.guard(true, h)) }
|
adm := func(pattern string, h http.HandlerFunc) { mux.HandleFunc(pattern, a.guard(true, h)) }
|
||||||
|
// full is for signed-in users and full-access tokens, even for reading.
|
||||||
|
full := func(pattern string, h http.HandlerFunc) { mux.HandleFunc(pattern, a.guard(false, fullAccess(h))) }
|
||||||
|
|
||||||
mux.HandleFunc("POST /api/v1/auth/login", a.login)
|
mux.HandleFunc("POST /api/v1/auth/login", a.login)
|
||||||
mux.HandleFunc("POST /api/v1/auth/logout", a.logout)
|
mux.HandleFunc("POST /api/v1/auth/logout", a.logout)
|
||||||
g("GET /api/v1/auth/me", a.me)
|
g("GET /api/v1/auth/me", a.me)
|
||||||
adm("POST /api/v1/auth/password", a.changePassword)
|
full("POST /api/v1/auth/password", a.changePassword)
|
||||||
adm("GET /api/v1/users", a.listUsers)
|
full("GET /api/v1/users", a.listUsers)
|
||||||
adm("POST /api/v1/users", a.createUser)
|
full("POST /api/v1/users", a.createUser)
|
||||||
adm("PATCH /api/v1/users/{id}", a.patchUser)
|
full("PATCH /api/v1/users/{id}", a.patchUser)
|
||||||
adm("POST /api/v1/users/{id}/reset-password", a.resetPassword)
|
full("POST /api/v1/users/{id}/reset-password", a.resetPassword)
|
||||||
adm("DELETE /api/v1/users/{id}", a.deleteUser)
|
full("DELETE /api/v1/users/{id}", a.deleteUser)
|
||||||
|
|
||||||
g("GET /api/v1/status", a.status)
|
g("GET /api/v1/status", a.status)
|
||||||
g("GET /api/v1/stats", a.allStats)
|
g("GET /api/v1/stats", a.allStats)
|
||||||
@@ -143,14 +156,14 @@ func (a *App) routes() http.Handler {
|
|||||||
mux.HandleFunc("GET /api/v1/setup/{token}", a.setupInfo)
|
mux.HandleFunc("GET /api/v1/setup/{token}", a.setupInfo)
|
||||||
mux.HandleFunc("POST /api/v1/setup/{token}", a.setupRedeem)
|
mux.HandleFunc("POST /api/v1/setup/{token}", a.setupRedeem)
|
||||||
|
|
||||||
// Full-access tokens (the iOS app) may change app settings and read logs.
|
// Full-access tokens (the iOS app) may change app settings, read logs and
|
||||||
// Users, passwords, tokens and backups need a signed-in user.
|
// manage users and tokens. Backups need a signed-in user.
|
||||||
g("GET /api/v1/settings", a.getSettings)
|
g("GET /api/v1/settings", a.getSettings)
|
||||||
g("PATCH /api/v1/settings", a.patchSettings)
|
g("PATCH /api/v1/settings", a.patchSettings)
|
||||||
g("POST /api/v1/restart", a.restart)
|
g("POST /api/v1/restart", a.restart)
|
||||||
adm("GET /api/v1/tokens", a.listTokens)
|
full("GET /api/v1/tokens", a.listTokens)
|
||||||
adm("POST /api/v1/tokens", a.createToken)
|
full("POST /api/v1/tokens", a.createToken)
|
||||||
adm("DELETE /api/v1/tokens/{id}", a.deleteToken)
|
full("DELETE /api/v1/tokens/{id}", a.deleteToken)
|
||||||
g("GET /api/v1/logs", a.logs)
|
g("GET /api/v1/logs", a.logs)
|
||||||
g("GET /api/v1/logs/download", a.downloadLog)
|
g("GET /api/v1/logs/download", a.downloadLog)
|
||||||
adm("GET /api/v1/backup", a.backup)
|
adm("GET /api/v1/backup", a.backup)
|
||||||
@@ -226,6 +239,9 @@ func (a *App) me(w http.ResponseWriter, r *http.Request) {
|
|||||||
"id": p.UserID, "name": p.Name, "isAdmin": p.IsAdmin, "scope": p.Scope,
|
"id": p.UserID, "name": p.Name, "isAdmin": p.IsAdmin, "scope": p.Scope,
|
||||||
"mustChangePassword": p.MustChangePassword, "version": version, "session": p.Session,
|
"mustChangePassword": p.MustChangePassword, "version": version, "session": p.Session,
|
||||||
}
|
}
|
||||||
|
if p.TokenID != "" {
|
||||||
|
out["tokenId"] = p.TokenID // lets an app find its own token in /tokens
|
||||||
|
}
|
||||||
if _, u := a.store.Get().userByID(p.UserID); u != nil {
|
if _, u := a.store.Get().userByID(p.UserID); u != nil {
|
||||||
out["username"], out["note"], out["created"] = u.Username, u.Note, u.Created
|
out["username"], out["note"], out["created"] = u.Username, u.Note, u.Created
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,6 +328,12 @@ func TestAPI(t *testing.T) {
|
|||||||
bearer("GET", "/tokens", 403)
|
bearer("GET", "/tokens", 403)
|
||||||
bearer("GET", "/peers/"+id+"/setup", 403) // the link would set up a device
|
bearer("GET", "/peers/"+id+"/setup", 403) // the link would set up a device
|
||||||
|
|
||||||
|
// A full-access token manages users and tokens, but not backups.
|
||||||
|
secret = call("POST", "/tokens", map[string]string{"name": "full", "scope": "rw"}, 201)["token"].(string)
|
||||||
|
bearer("GET", "/users", 200)
|
||||||
|
bearer("GET", "/tokens", 200)
|
||||||
|
bearer("GET", "/backup", 403)
|
||||||
|
|
||||||
call("DELETE", "/peers/"+id, nil, 200)
|
call("DELETE", "/peers/"+id, nil, 200)
|
||||||
if len(store.Get().Peers) != 0 {
|
if len(store.Get().Peers) != 0 {
|
||||||
t.Fatal("peer not deleted")
|
t.Fatal("peer not deleted")
|
||||||
|
|||||||
Reference in New Issue
Block a user