state: allow untagging nodes via reauth with empty RequestTags
When a node re-authenticates via OIDC/web auth with empty RequestTags (from `tailscale up --advertise-tags= --force-reauth`), remove all tags and return ownership to the authenticating user. This allows nodes to transition from any tagged state (including nodes originally registered with a tagged pre-auth key) back to user-owned. Fixes #2979
This commit is contained in:
parent
a6696582a4
commit
0451dd4718
3 changed files with 302 additions and 4 deletions
|
|
@ -1246,6 +1246,93 @@ func (s *State) createAndSaveNewNode(params newNodeParams) (types.NodeView, erro
|
|||
return s.nodeStore.PutNode(*savedNode), nil
|
||||
}
|
||||
|
||||
// processReauthTags handles tag changes during node re-authentication.
|
||||
// It processes RequestTags from the client and updates node tags accordingly.
|
||||
// Returns rejected tags (if any) for post-validation error handling.
|
||||
func (s *State) processReauthTags(
|
||||
node *types.Node,
|
||||
requestTags []string,
|
||||
user *types.User,
|
||||
oldTags []string,
|
||||
) []string {
|
||||
wasAuthKeyTagged := node.AuthKey != nil && node.AuthKey.IsTagged()
|
||||
|
||||
logEvent := log.Debug().
|
||||
Uint64("node.id", uint64(node.ID)).
|
||||
Str("node.name", node.Hostname).
|
||||
Strs("request.tags", requestTags).
|
||||
Strs("current.tags", node.Tags).
|
||||
Bool("is.tagged", node.IsTagged()).
|
||||
Bool("was.authkey.tagged", wasAuthKeyTagged)
|
||||
logEvent.Msg("Processing RequestTags during reauth")
|
||||
|
||||
// Empty RequestTags means untag node (transition to user-owned)
|
||||
if len(requestTags) == 0 {
|
||||
if node.IsTagged() {
|
||||
log.Info().
|
||||
Uint64("node.id", uint64(node.ID)).
|
||||
Str("node.name", node.Hostname).
|
||||
Strs("removed.tags", node.Tags).
|
||||
Str("user.name", user.Name).
|
||||
Bool("was.authkey.tagged", wasAuthKeyTagged).
|
||||
Msg("Reauth: removing all tags, returning node ownership to user")
|
||||
|
||||
node.Tags = []string{}
|
||||
node.UserID = &user.ID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Non-empty RequestTags: validate and apply
|
||||
var approvedTags, rejectedTags []string
|
||||
|
||||
for _, tag := range requestTags {
|
||||
if s.polMan.NodeCanHaveTag(node.View(), tag) {
|
||||
approvedTags = append(approvedTags, tag)
|
||||
} else {
|
||||
rejectedTags = append(rejectedTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rejectedTags) > 0 {
|
||||
log.Warn().
|
||||
Uint64("node.id", uint64(node.ID)).
|
||||
Str("node.name", node.Hostname).
|
||||
Strs("rejected.tags", rejectedTags).
|
||||
Msg("Reauth: requested tags are not permitted")
|
||||
|
||||
return rejectedTags
|
||||
}
|
||||
|
||||
if len(approvedTags) > 0 {
|
||||
slices.Sort(approvedTags)
|
||||
approvedTags = slices.Compact(approvedTags)
|
||||
|
||||
wasTagged := node.IsTagged()
|
||||
node.Tags = approvedTags
|
||||
|
||||
// Note: UserID is preserved as "created by" tracking, consistent with SetNodeTags
|
||||
if !wasTagged {
|
||||
log.Info().
|
||||
Uint64("node.id", uint64(node.ID)).
|
||||
Str("node.name", node.Hostname).
|
||||
Strs("new.tags", approvedTags).
|
||||
Str("old.user", user.Name).
|
||||
Msg("Reauth: applying tags, transferring node to tagged-devices")
|
||||
} else {
|
||||
log.Info().
|
||||
Uint64("node.id", uint64(node.ID)).
|
||||
Str("node.name", node.Hostname).
|
||||
Strs("old.tags", oldTags).
|
||||
Strs("new.tags", approvedTags).
|
||||
Msg("Reauth: updating tags on already-tagged node")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleNodeFromAuthPath handles node registration through authentication flow (like OIDC).
|
||||
func (s *State) HandleNodeFromAuthPath(
|
||||
registrationID types.RegistrationID,
|
||||
|
|
@ -1291,14 +1378,26 @@ func (s *State) HandleNodeFromAuthPath(
|
|||
|
||||
// If this node exists for this user, update the node in place.
|
||||
if existsSameUser && existingNodeSameUser.Valid() {
|
||||
log.Debug().
|
||||
log.Info().
|
||||
Caller().
|
||||
Str("registration_id", registrationID.String()).
|
||||
Str("user.name", user.Name).
|
||||
Str("registrationMethod", registrationMethod).
|
||||
Str("node.name", existingNodeSameUser.Hostname()).
|
||||
Uint64("node.id", existingNodeSameUser.ID().Uint64()).
|
||||
Msg("Updating existing node registration")
|
||||
Interface("hostinfo", regEntry.Node.Hostinfo).
|
||||
Msg("Updating existing node registration via reauth")
|
||||
|
||||
// Process RequestTags during reauth (#2979)
|
||||
// Due to json:",omitempty", we treat empty/nil as "clear tags"
|
||||
var requestTags []string
|
||||
if regEntry.Node.Hostinfo != nil {
|
||||
requestTags = regEntry.Node.Hostinfo.RequestTags
|
||||
}
|
||||
|
||||
oldTags := existingNodeSameUser.Tags().AsSlice()
|
||||
|
||||
var rejectedTags []string
|
||||
|
||||
// Update existing node - NodeStore first, then database
|
||||
updatedNodeView, ok := s.nodeStore.UpdateNode(existingNodeSameUser.ID(), func(node *types.Node) {
|
||||
|
|
@ -1327,12 +1426,18 @@ func (s *State) HandleNodeFromAuthPath(
|
|||
node.Expiry = regEntry.Node.Expiry
|
||||
}
|
||||
}
|
||||
|
||||
rejectedTags = s.processReauthTags(node, requestTags, user, oldTags)
|
||||
})
|
||||
|
||||
if !ok {
|
||||
return types.NodeView{}, change.Change{}, fmt.Errorf("%w: %d", ErrNodeNotInNodeStore, existingNodeSameUser.ID())
|
||||
}
|
||||
|
||||
if len(rejectedTags) > 0 {
|
||||
return types.NodeView{}, change.Change{}, fmt.Errorf("%w %v are invalid or not permitted", ErrRequestedTagsInvalidOrNotPermitted, rejectedTags)
|
||||
}
|
||||
|
||||
_, err = hsdb.Write(s.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
// Use Updates() to preserve fields not modified by UpdateNode.
|
||||
err := tx.Updates(updatedNodeView.AsStruct()).Error
|
||||
|
|
@ -1686,6 +1791,14 @@ func (s *State) updatePolicyManagerUsers() (change.Change, error) {
|
|||
return change.Change{}, nil
|
||||
}
|
||||
|
||||
// UpdatePolicyManagerUsersForTest updates the policy manager's user cache.
|
||||
// This is exposed for testing purposes to sync the policy manager after
|
||||
// creating test users via CreateUserForTest().
|
||||
func (s *State) UpdatePolicyManagerUsersForTest() error {
|
||||
_, err := s.updatePolicyManagerUsers()
|
||||
return err
|
||||
}
|
||||
|
||||
// updatePolicyManagerNodes updates the policy manager with current nodes.
|
||||
// Returns true if the policy changed and notifications should be sent.
|
||||
// TODO(kradalby): This is a temporary stepping stone, ultimately we should
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue