Limit token permissions

Added the ability to scope tokens to a specific client. If
enabled, this has the affect of limiting the token to being used
to list tunnels for that specific client. It can't be used for
the web UI or for any state-changing actions such as creating
new tunnels.
This commit is contained in:
Anders Pitman 2022-02-16 11:44:24 -07:00
parent 392a1ec8d7
commit 0a23c2fc0e
6 changed files with 106 additions and 12 deletions

63
api.go
View File

@ -59,8 +59,24 @@ func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) {
tunnels := a.GetTunnels(tokenData)
if len(query["client-name"]) == 1 {
clientName := query["client-name"][0]
// If the token is limited to a specific client, filter out
// tunnels for any other clients.
if tokenData.Client != "" {
for k, tun := range tunnels {
if tokenData.Client != tun.ClientName {
delete(tunnels, k)
}
}
}
clientName := query.Get("client-name")
if clientName != "" && tokenData.Client != "" && clientName != tokenData.Client {
w.WriteHeader(403)
w.Write([]byte("Token is not valid for this client"))
return
}
if clientName != "" {
for k, tun := range tunnels {
if tun.ClientName != clientName {
delete(tunnels, k)
@ -85,6 +101,13 @@ func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(body))
case "POST":
if tokenData.Client != "" {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("Token can only be used to list tunnels for client %s", tokenData.Client))
return
}
r.ParseForm()
_, err := a.CreateTunnel(tokenData, r.Form)
if err != nil {
@ -92,6 +115,12 @@ func (a *Api) handleTunnels(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(err.Error()))
}
case "DELETE":
if tokenData.Client != "" {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("Token can only be used to list tunnels for client %s", tokenData.Client))
return
}
r.ParseForm()
err := a.DeleteTunnel(tokenData, r.Form)
if err != nil {
@ -119,6 +148,12 @@ func (a *Api) handleUsers(w http.ResponseWriter, r *http.Request) {
return
}
if tokenData.Client != "" {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("Token can only be used to list tunnels for client %s", tokenData.Client))
return
}
path := r.URL.Path
parts := strings.Split(path[1:], "/")
@ -178,6 +213,12 @@ func (a *Api) handleTokens(w http.ResponseWriter, r *http.Request) {
return
}
if tokenData.Client != "" {
w.WriteHeader(403)
io.WriteString(w, fmt.Sprintf("Token can only be used to list tunnels for client %s", tokenData.Client))
return
}
switch r.Method {
case "POST":
r.ParseForm()
@ -326,6 +367,7 @@ func (a *Api) CreateTunnel(tokenData TokenData, params url.Values) (*Tunnel, err
}
func (a *Api) DeleteTunnel(tokenData TokenData, params url.Values) error {
domain := params.Get("domain")
if domain == "" {
return errors.New("Invalid domain parameter")
@ -355,14 +397,21 @@ func (a *Api) CreateToken(tokenData TokenData, params url.Values) (string, error
return "", errors.New("Invalid owner paramater")
}
if tokenData.Owner != owner {
user, _ := a.db.GetUser(tokenData.Owner)
if !user.IsAdmin {
return "", errors.New("Unauthorized")
user, _ := a.db.GetUser(tokenData.Owner)
if tokenData.Owner != owner && !user.IsAdmin {
return "", errors.New("Unauthorized")
}
client := params.Get("client")
if client != "any" {
if _, exists := user.Clients[client]; !exists {
return "", errors.New(fmt.Sprintf("Client %s does not exist for user %s", client, owner))
}
}
token, err := a.db.AddToken(owner)
token, err := a.db.AddToken(owner, client)
if err != nil {
return "", errors.New("Failed to create token")
}

View File

@ -128,7 +128,7 @@ func Listen() {
users := db.GetUsers()
if len(users) == 0 {
db.AddUser("admin", true)
_, err := db.AddToken("admin")
_, err := db.AddToken("admin", "any")
if err != nil {
log.Fatal("Failed to initialize admin user")
}

View File

@ -20,7 +20,8 @@ type Database struct {
}
type TokenData struct {
Owner string `json:"owner"`
Owner string `json:"owner"`
Client string `json:"client,omitempty"`
}
type User struct {
@ -142,7 +143,7 @@ func (d *Database) DeleteDNSRequest(requestId string) {
delete(d.dnsRequests, requestId)
}
func (d *Database) AddToken(owner string) (string, error) {
func (d *Database) AddToken(owner, client string) (string, error) {
d.mutex.Lock()
defer d.mutex.Unlock()
@ -156,7 +157,10 @@ func (d *Database) AddToken(owner string) (string, error) {
return "", errors.New("Could not generat token")
}
d.Tokens[token] = TokenData{owner}
d.Tokens[token] = TokenData{
Owner: owner,
Client: client,
}
d.persist()

View File

@ -0,0 +1,14 @@
{{ template "header.tmpl" . }}
<h1>Add Token</h1>
<form action="/tokens" method="POST">
<input type="hidden" name="owner" value="{{$.Owner}}">
<label for="token-client">Limit to client:</label>
<select id="token-client" name="client">
<option value="any">No</option>
{{range $clientName, $c := $.User.Clients}}
<option value="{{$clientName}}">{{$clientName}}</option>
{{end}}
</select>
<button class='button' type="submit">Submit</button>
</form>
{{ template "footer.tmpl" . }}

View File

@ -14,7 +14,7 @@
</div>
<div class='token-adder'>
<form action="/tokens" method="POST">
<form action="/add-token-client" method="POST">
<label for="token-owner">Owner:</label>
<select id="token-owner" name="owner">
{{range $username, $user := .Users}}

View File

@ -89,6 +89,12 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
return
}
if tokenData.Client != "" {
w.WriteHeader(403)
h.alertDialog(w, r, "This token is limited to a specific client and cannot be used for the web UI", "/")
return
}
user, _ := h.db.GetUser(tokenData.Owner)
tunnels := h.api.GetTunnels(tokenData)
@ -206,6 +212,27 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
w.Header().Set("Content-Disposition", "attachment; filename=id_rsa")
io.WriteString(w, tun.TunnelPrivateKey)
case "/add-token-client":
r.ParseForm()
owner := r.Form.Get("owner")
addTokenUser, _ := h.db.GetUser(owner)
templateData := struct {
Owner string
User User
}{
Owner: owner,
User: addTokenUser,
}
err := h.tmpl.ExecuteTemplate(w, "add_token_client.tmpl", templateData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
case "/tokens":
h.handleTokens(w, r, user, tokenData)
case "/confirm-delete-token":