Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 27 additions & 14 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package tscaddy
// auth.go contains the TailscaleAuth module and supporting logic.

import (
"fmt"
"net"
"net/http"
"reflect"
Expand Down Expand Up @@ -126,12 +125,18 @@ type tsnetListener interface {

// Authenticate authenticates the request and sets Tailscale user data on the caddy User object.
//
// This method will set the following user metadata:
// For regular (non-tagged) users, the following metadata is set:
// - tailscale_login: the user's login name without the domain
// - tailscale_user: the user's full login name
// - tailscale_name: the user's display name
// - tailscale_profile_picture: the user's profile picture URL
// - tailscale_tailnet: the user's tailnet name (if the user is not connecting to a shared node)
// - tailscale_tailnet: the user's tailnet name
//
// For tagged nodes, the following metadata is set:
// - tailscale_tags: comma-separated list of node tags (e.g. "tag:zergbot,tag:ci")
// - tailscale_tailnet: the tailnet name
//
// The user ID is set to the login name for regular users, or the first tag for tagged nodes.
func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.User, bool, error) {
user := caddyauth.User{}

Expand All @@ -145,25 +150,33 @@ func (ta Auth) Authenticate(w http.ResponseWriter, r *http.Request) (caddyauth.U
return user, false, err
}

if len(info.Node.Tags) != 0 {
return user, false, fmt.Errorf("node %s has tags", info.Node.Hostinfo.Hostname())
}

var tailnet string
if !info.Node.Hostinfo.ShareeNode() {
if s, found := strings.CutPrefix(info.Node.Name, info.Node.ComputedName+"."); found {
tailnet = strings.TrimSuffix(s, ".")
}
}

user.ID = info.UserProfile.LoginName
user.Metadata = map[string]string{
"tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0],
"tailscale_user": info.UserProfile.LoginName,
"tailscale_name": info.UserProfile.DisplayName,
"tailscale_profile_picture": info.UserProfile.ProfilePicURL,
"tailscale_tailnet": tailnet,
if len(info.Node.Tags) != 0 {
// Tagged node: expose tags as metadata for Caddy expression matching.
// user.ID is set to the first tag for convenient @matcher expressions.
user.ID = info.Node.Tags[0]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the one part I'm uncertain about, and is largely what stalled #2 . We've never really decided how to identify tagged nodes, and I never felt comfortable just using the first tag.

Interestingly, in Tailscale Serve, we just don't populate identity headers at all. We probably don't need to go that far... a lot of these other headers are super useful, especially tags.

I wonder if it would break things if we just didn't populate user.ID at all in this case? I worry that this would be uncommon enough that downstream users may assume that it's always present. If we have to pick something, I think I'd prefer we use the device name, probably prefixed with device: to make clear that this is not a regular user.

user.Metadata = map[string]string{
"tailscale_tags": strings.Join(info.Node.Tags, ","),
"tailscale_tailnet": tailnet,
}
} else {
user.ID = info.UserProfile.LoginName
user.Metadata = map[string]string{
"tailscale_login": strings.Split(info.UserProfile.LoginName, "@")[0],
"tailscale_user": info.UserProfile.LoginName,
"tailscale_name": info.UserProfile.DisplayName,
"tailscale_profile_picture": info.UserProfile.ProfilePicURL,
"tailscale_tailnet": tailnet,
"tailscale_tags": "",
}
}

return user, true, nil
}

Expand Down
Loading