cli: accept a user name in preauthkeys create --user

Resolved through lookupUser like the users commands, so scripts skip the
users list round trip. Digit-only values stay IDs.
This commit is contained in:
Kristoffer Dalby
2026-09-28 13:34:46 +00:00
committed by Kristoffer Dalby
parent 715c5a4a1c
commit 62f89ca25d
6 changed files with 86 additions and 7 deletions
+1
View File
@@ -94,6 +94,7 @@ removed on this schedule:
- Fix `headscale users destroy`/`rename` reporting "multiple users match query" when no user matches; an ambiguous match now lists the matching users [#3476](https://github.com/juanfont/headscale/pull/3476)
- Deleting a user that still owns nodes now lists the nodes (ID and hostname) that must be deleted first [#3475](https://github.com/juanfont/headscale/pull/3475)
- Headscale now requires Go 1.27 to build
- `headscale preauthkeys create --user` accepts a user name as well as an ID
- `derp.paths` files may be Tailscale JSON or HuJSON DERP maps as well as YAML
- `dns.extra_records_path` files may be HuJSON or YAML as well as JSON
- A `derp.paths` region set to `null` removes that region again, as documented
+6 -3
View File
@@ -30,7 +30,7 @@ func init() {
StringP("expiration", "e", DefaultPreAuthKeyExpiry, "Human-readable expiration of the key (e.g. 30m, 24h)")
createPreAuthKeyCmd.Flags().
StringSlice("tags", []string{}, "Tags to automatically assign to node")
createPreAuthKeyCmd.PersistentFlags().Uint64P("user", "u", 0, "User identifier (ID)")
createPreAuthKeyCmd.PersistentFlags().StringP("user", "u", "", "User ID, or name if not a number")
expirePreAuthKeyCmd.PersistentFlags().Uint64P("id", "i", 0, "Authkey ID")
deletePreAuthKeyCmd.PersistentFlags().Uint64P("id", "i", 0, "Authkey ID")
}
@@ -102,7 +102,7 @@ var createPreAuthKeyCmd = &cobra.Command{
Short: "Creates a new preauthkey",
Aliases: []string{"c", cmdNew},
RunE: clientRunE(func(ctx context.Context, client *clientv1.ClientWithResponses, cmd *cobra.Command, args []string) error {
user, _ := cmd.Flags().GetUint64("user")
userArg, _ := cmd.Flags().GetString("user")
reusable, _ := cmd.Flags().GetBool("reusable")
ephemeral, _ := cmd.Flags().GetBool("ephemeral")
tags, _ := cmd.Flags().GetStringSlice("tags")
@@ -112,7 +112,10 @@ var createPreAuthKeyCmd = &cobra.Command{
return err
}
userStr := strconv.FormatUint(user, util.Base10)
userStr, err := userIDFromArg(ctx, client, userArg)
if err != nil {
return err
}
request := clientv1.CreatePreAuthKeyJSONRequestBody{
User: &userStr,
+24
View File
@@ -67,6 +67,30 @@ func resolveSingleUser(
return lookupUser(ctx, client, id, username)
}
// userIDFromArg resolves a --user value: an ID, or a user name when it is not
// a number, so a user whose name is all digits must be given by ID.
func userIDFromArg(
ctx context.Context,
client *clientv1.ClientWithResponses,
arg string,
) (string, error) {
if arg == "" {
return "", nil
}
_, err := strconv.ParseUint(arg, util.Base10, 64)
if err == nil {
return arg, nil
}
id, _, err := lookupUser(ctx, client, 0, arg)
if err != nil {
return "", fmt.Errorf("--user %q: %w", arg, err)
}
return id, nil
}
// lookupUser resolves exactly one user by ID and/or name (0 and "" are unset),
// returning the identifier of the matched user and the user itself.
func lookupUser(
+51
View File
@@ -184,3 +184,54 @@ func TestResolveSingleUser(t *testing.T) {
})
}
}
func TestUserIDFromArg(t *testing.T) {
alice := clientv1.User{Id: "3", Name: "alice"}
digits := clientv1.User{Id: "7", Name: "42"}
aliceDup := clientv1.User{Id: "8", Name: "alice"}
tests := []struct {
name string
users []clientv1.User
arg string
wantID string
wantErr bool
}{
{name: "unset stays unset", arg: "", wantID: ""},
{name: "number is an ID", users: []clientv1.User{alice}, arg: "3", wantID: "3"},
// Numbers never hit the name lookup, so user "42" needs its ID.
{name: "digit-only name is an ID", users: []clientv1.User{digits}, arg: "42", wantID: "42"},
{name: "name resolves to its ID", users: []clientv1.User{alice, digits}, arg: "alice", wantID: "3"},
{name: "unknown name is an error", users: []clientv1.User{alice}, arg: "bob", wantErr: true},
{name: "ambiguous name is an error", users: []clientv1.User{alice, aliceDup}, arg: "alice", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := filterUsersServer(t, tt.users)
defer server.Close()
client, err := clientv1.NewClientWithResponses(server.URL)
if err != nil {
t.Fatalf("creating client: %v", err)
}
id, err := userIDFromArg(context.Background(), client, tt.arg)
if tt.wantErr {
if err == nil {
t.Fatalf("userIDFromArg(%q) error = nil, want error", tt.arg)
}
return
}
if err != nil {
t.Fatalf("userIDFromArg(%q) error = %v", tt.arg, err)
}
if id != tt.wantID {
t.Errorf("userIDFromArg(%q) = %q, want %q", tt.arg, id, tt.wantID)
}
})
}
}
+2 -2
View File
@@ -105,10 +105,10 @@ Its best suited for automation.
headscale users create <USER>
```
Use the `headscale user list` command to learn its `<USER_ID>` and create a new pre authenticated key for your user:
Create a new pre authenticated key for your user, by name or by the `<USER_ID>` that `headscale user list` shows:
```console
headscale preauthkeys create --user <USER_ID>
headscale preauthkeys create --user <USER>
```
The above prints a pre authenticated key with the default settings (can be used once and is valid for one hour). Use
+2 -2
View File
@@ -133,14 +133,14 @@ headscale instance. By default, the key is valid for one hour and can only be us
=== "Native"
```shell
headscale preauthkeys create --user <USER_ID>
headscale preauthkeys create --user <USER>
```
=== "Container"
```shell
docker exec -it headscale \
headscale preauthkeys create --user <USER_ID>
headscale preauthkeys create --user <USER>
```
The command returns the preauthkey on success which is used to connect a node to the headscale instance via the