diff --git a/CHANGELOG.md b/CHANGELOG.md index e62f3702..aead3897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index f6d4ad2b..37740f39 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -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, diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index 203f129c..7d5cfb0e 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -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( diff --git a/cmd/headscale/cli/users_test.go b/cmd/headscale/cli/users_test.go index 5ed0a7db..fbe68c22 100644 --- a/cmd/headscale/cli/users_test.go +++ b/cmd/headscale/cli/users_test.go @@ -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) + } + }) + } +} diff --git a/docs/ref/registration.md b/docs/ref/registration.md index 81f4c06b..c2db42bd 100644 --- a/docs/ref/registration.md +++ b/docs/ref/registration.md @@ -105,10 +105,10 @@ Its best suited for automation. headscale users create ``` - Use the `headscale user list` command to learn its `` and create a new pre authenticated key for your user: + Create a new pre authenticated key for your user, by name or by the `` that `headscale user list` shows: ```console - headscale preauthkeys create --user + headscale preauthkeys create --user ``` The above prints a pre authenticated key with the default settings (can be used once and is valid for one hour). Use diff --git a/docs/usage/getting-started.md b/docs/usage/getting-started.md index 3171c348..f826b5f4 100644 --- a/docs/usage/getting-started.md +++ b/docs/usage/getting-started.md @@ -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 + headscale preauthkeys create --user ``` === "Container" ```shell docker exec -it headscale \ - headscale preauthkeys create --user + headscale preauthkeys create --user ``` The command returns the preauthkey on success which is used to connect a node to the headscale instance via the