integration: support auth keys without user

Add AuthKeyOptions to create auth keys owned by tags only.
This commit is contained in:
Kristoffer Dalby 2026-01-07 12:12:53 +01:00 committed by Kristoffer Dalby
parent 3b4b9a4436
commit 740d2b5a2c
3 changed files with 70 additions and 53 deletions

View file

@ -8,6 +8,7 @@ import (
policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2" policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
"github.com/juanfont/headscale/hscontrol/routes" "github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/integration/hsic"
"github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
) )
@ -25,6 +26,7 @@ type ControlServer interface {
CreateUser(user string) (*v1.User, error) CreateUser(user string) (*v1.User, error)
CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error) CreateAuthKey(user uint64, reusable bool, ephemeral bool) (*v1.PreAuthKey, error)
CreateAuthKeyWithTags(user uint64, reusable bool, ephemeral bool, tags []string) (*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 DeleteAuthKey(user uint64, key string) error
ListNodes(users ...string) ([]*v1.Node, error) ListNodes(users ...string) ([]*v1.Node, error)
DeleteNode(nodeID uint64) error DeleteNode(nodeID uint64) error

View file

@ -1067,33 +1067,52 @@ func (t *HeadscaleInContainer) CreateUser(
return &u, nil return &u, nil
} }
// CreateAuthKey creates a new "authorisation key" for a User that can be used // AuthKeyOptions defines options for creating an auth key.
// to authorise a TailscaleClient with the Headscale instance. type AuthKeyOptions struct {
func (t *HeadscaleInContainer) CreateAuthKey( // User is the user ID that owns the auth key. If nil and Tags are specified,
user uint64, // the auth key is owned by the tags only (tags-as-identity model).
reusable bool, User *uint64
ephemeral bool, // Reusable indicates if the key can be used multiple times
) (*v1.PreAuthKey, error) { 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{ command := []string{
"headscale", "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", "preauthkeys",
"create", "create",
"--expiration", "--expiration",
"24h", "24h",
"--output", "--output",
"json", "json",
} )
if reusable { if opts.Reusable {
command = append(command, "--reusable") command = append(command, "--reusable")
} }
if ephemeral { if opts.Ephemeral {
command = append(command, "--ephemeral") command = append(command, "--ephemeral")
} }
if len(opts.Tags) > 0 {
command = append(command, "--tags", strings.Join(opts.Tags, ","))
}
result, _, err := dockertestutil.ExecuteCommand( result, _, err := dockertestutil.ExecuteCommand(
t.container, t.container,
command, command,
@ -1104,6 +1123,7 @@ func (t *HeadscaleInContainer) CreateAuthKey(
} }
var preAuthKey v1.PreAuthKey var preAuthKey v1.PreAuthKey
err = json.Unmarshal([]byte(result), &preAuthKey) err = json.Unmarshal([]byte(result), &preAuthKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal auth key: %w", err) return nil, fmt.Errorf("failed to unmarshal auth key: %w", err)
@ -1112,6 +1132,20 @@ func (t *HeadscaleInContainer) CreateAuthKey(
return &preAuthKey, nil 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. // 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. // This is used to create tagged PreAuthKeys for testing the tags-as-identity model.
func (t *HeadscaleInContainer) CreateAuthKeyWithTags( func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
@ -1120,47 +1154,12 @@ func (t *HeadscaleInContainer) CreateAuthKeyWithTags(
ephemeral bool, ephemeral bool,
tags []string, tags []string,
) (*v1.PreAuthKey, error) { ) (*v1.PreAuthKey, error) {
command := []string{ return t.CreateAuthKeyWithOptions(AuthKeyOptions{
"headscale", User: &user,
"--user", Reusable: reusable,
strconv.FormatUint(user, 10), Ephemeral: ephemeral,
"preauthkeys", Tags: tags,
"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
} }
// DeleteAuthKey deletes an "authorisation key" for a User. // DeleteAuthKey deletes an "authorisation key" for a User.

View file

@ -478,6 +478,22 @@ func (s *Scenario) CreatePreAuthKey(
return nil, fmt.Errorf("failed to create user: %w", errNoHeadscaleAvailable) 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 // CreatePreAuthKeyWithTags creates a "pre authorised key" with the specified tags
// to be created in the Headscale instance on behalf of the Scenario. // to be created in the Headscale instance on behalf of the Scenario.
func (s *Scenario) CreatePreAuthKeyWithTags( func (s *Scenario) CreatePreAuthKeyWithTags(