From 740d2b5a2c5cdf8c31378ab2bd0d129ffafc943d Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 7 Jan 2026 12:12:53 +0100 Subject: [PATCH] integration: support auth keys without user Add AuthKeyOptions to create auth keys owned by tags only. --- integration/control.go | 2 + integration/hsic/hsic.go | 105 +++++++++++++++++++-------------------- integration/scenario.go | 16 ++++++ 3 files changed, 70 insertions(+), 53 deletions(-) diff --git a/integration/control.go b/integration/control.go index 2b3b9cb3..74f98ced 100644 --- a/integration/control.go +++ b/integration/control.go @@ -8,6 +8,7 @@ import ( policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" "github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/integration/hsic" "github.com/ory/dockertest/v3" "tailscale.com/tailcfg" ) @@ -25,6 +26,7 @@ type ControlServer interface { CreateUser(user string) (*v1.User, error) CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*v1.PreAuthKey, error) + CreateAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error) DeleteAuthKey(user uint64, key string) error ListNodes(users ...string) ([]*v1.Node, error) DeleteNode(nodeID uint64) error diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 9e8e5656..787c244f 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -1067,33 +1067,52 @@ func (t *HeadscaleInContainer) CreateUser( return &u, nil } -// CreateAuthKey creates a new "authorisation key" for a User that can be used -// to authorise a TailscaleClient with the Headscale instance. -func (t *HeadscaleInContainer) CreateAuthKey( - user uint64, - reusable bool, - ephemeral bool, -) (*v1.PreAuthKey, error) { +// AuthKeyOptions defines options for creating an auth key. +type AuthKeyOptions struct { + // User is the user ID that owns the auth key. If nil and Tags are specified, + // the auth key is owned by the tags only (tags-as-identity model). + User *uint64 + // Reusable indicates if the key can be used multiple times + Reusable bool + // Ephemeral indicates if nodes registered with this key should be ephemeral + Ephemeral bool + // Tags are the tags to assign to the auth key + Tags []string +} + +// CreateAuthKeyWithOptions creates a new "authorisation key" with the specified options. +// This supports both user-owned and tags-only auth keys. +func (t *HeadscaleInContainer) CreateAuthKeyWithOptions(opts AuthKeyOptions) (*v1.PreAuthKey, error) { command := []string{ "headscale", - "--user", - strconv.FormatUint(user, 10), + } + + // Only add --user flag if User is specified + if opts.User != nil { + command = append(command, "--user", strconv.FormatUint(*opts.User, 10)) + } + + command = append(command, "preauthkeys", "create", "--expiration", "24h", "--output", "json", - } + ) - if reusable { + if opts.Reusable { command = append(command, "--reusable") } - if ephemeral { + if opts.Ephemeral { command = append(command, "--ephemeral") } + if len(opts.Tags) > 0 { + command = append(command, "--tags", strings.Join(opts.Tags, ",")) + } + result, _, err := dockertestutil.ExecuteCommand( t.container, command, @@ -1104,6 +1123,7 @@ func (t *HeadscaleInContainer) CreateAuthKey( } var preAuthKey v1.PreAuthKey + err = json.Unmarshal([]byte(result), &preAuthKey) if err != nil { return nil, fmt.Errorf("failed to unmarshal auth key: %w", err) @@ -1112,6 +1132,20 @@ func (t *HeadscaleInContainer) CreateAuthKey( return &preAuthKey, nil } +// CreateAuthKey creates a new "authorisation key" for a User that can be used +// to authorise a TailscaleClient with the Headscale instance. +func (t *HeadscaleInContainer) CreateAuthKey( + user uint64, + reusable bool, + ephemeral bool, +) (*v1.PreAuthKey, error) { + return t.CreateAuthKeyWithOptions(AuthKeyOptions{ + User: &user, + Reusable: reusable, + Ephemeral: ephemeral, + }) +} + // CreateAuthKeyWithTags creates a new "authorisation key" for a User with the specified tags. // This is used to create tagged PreAuthKeys for testing the tags-as-identity model. func (t *HeadscaleInContainer) CreateAuthKeyWithTags( @@ -1120,47 +1154,12 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithTags( ephemeral bool, tags []string, ) (*v1.PreAuthKey, error) { - command := []string{ - "headscale", - "--user", - strconv.FormatUint(user, 10), - "preauthkeys", - "create", - "--expiration", - "24h", - "--output", - "json", - } - - if reusable { - command = append(command, "--reusable") - } - - if ephemeral { - command = append(command, "--ephemeral") - } - - if len(tags) > 0 { - command = append(command, "--tags", strings.Join(tags, ",")) - } - - result, _, err := dockertestutil.ExecuteCommand( - t.container, - command, - []string{}, - ) - if err != nil { - return nil, fmt.Errorf("failed to execute create auth key with tags command: %w", err) - } - - var preAuthKey v1.PreAuthKey - - err = json.Unmarshal([]byte(result), &preAuthKey) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal auth key: %w", err) - } - - return &preAuthKey, nil + return t.CreateAuthKeyWithOptions(AuthKeyOptions{ + User: &user, + Reusable: reusable, + Ephemeral: ephemeral, + Tags: tags, + }) } // DeleteAuthKey deletes an "authorisation key" for a User. diff --git a/integration/scenario.go b/integration/scenario.go index cc3cf968..35fee73e 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -478,6 +478,22 @@ func (s *Scenario) CreatePreAuthKey( return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable) } +// CreatePreAuthKeyWithOptions creates a "pre authorised key" with the specified options +// to be created in the Headscale instance on behalf of the Scenario. +func (s *Scenario) CreatePreAuthKeyWithOptions(opts hsic.AuthKeyOptions) (*v1.PreAuthKey, error) { + headscale, err := s.Headscale() + if err != nil { + return nil, fmt.Errorf("failed to create preauth key with options: %w", errNoHeadscaleAvailable) + } + + key, err := headscale.CreateAuthKeyWithOptions(opts) + if err != nil { + return nil, fmt.Errorf("failed to create preauth key with options: %w", err) + } + + return key, nil +} + // CreatePreAuthKeyWithTags creates a "pre authorised key" with the specified tags // to be created in the Headscale instance on behalf of the Scenario. func (s *Scenario) CreatePreAuthKeyWithTags(