diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index ec803c61..4032b82d 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -12,12 +12,43 @@ import ( "google.golang.org/grpc/status" ) +func usernameAndIDFlag(cmd *cobra.Command) { + cmd.Flags().Int64P("identifier", "i", -1, "User identifier (ID)") + cmd.Flags().StringP("name", "n", "", "Username") +} + +// usernameAndIDFromFlag returns the username and ID from the flags of the command. +// If both are empty, it will exit the program with an error. +func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) { + username, _ := cmd.Flags().GetString("name") + identifier, _ := cmd.Flags().GetInt64("identifier") + if username == "" && identifier < 0 { + err := errors.New("--name or --identifier flag is required") + ErrorOutput( + err, + fmt.Sprintf( + "Cannot rename user: %s", + status.Convert(err).Message(), + ), + "", + ) + } + + return uint64(identifier), username +} + func init() { rootCmd.AddCommand(userCmd) userCmd.AddCommand(createUserCmd) userCmd.AddCommand(listUsersCmd) + usernameAndIDFlag(listUsersCmd) + listUsersCmd.Flags().StringP("email", "e", "", "Email") userCmd.AddCommand(destroyUserCmd) + usernameAndIDFlag(destroyUserCmd) userCmd.AddCommand(renameUserCmd) + usernameAndIDFlag(renameUserCmd) + renameUserCmd.Flags().StringP("new-name", "r", "", "New username") + renameNodeCmd.MarkFlagRequired("new-name") } var errMissingParameter = errors.New("missing parameters") @@ -70,30 +101,23 @@ var createUserCmd = &cobra.Command{ } var destroyUserCmd = &cobra.Command{ - Use: "destroy NAME", + Use: "destroy --identifier ID or --name NAME", Short: "Destroys a user", Aliases: []string{"delete"}, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return errMissingParameter - } - - return nil - }, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - userName := args[0] - - request := &v1.GetUserRequest{ - Name: userName, + id, username := usernameAndIDFromFlag(cmd) + request := &v1.ListUsersRequest{ + Name: username, + Id: id, } ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() - _, err := client.GetUser(ctx, request) + users, err := client.ListUsers(ctx, request) if err != nil { ErrorOutput( err, @@ -102,13 +126,24 @@ var destroyUserCmd = &cobra.Command{ ) } + if len(users.GetUsers()) != 1 { + err := fmt.Errorf("Unable to determine user to delete, query returned multiple users, use ID") + ErrorOutput( + err, + fmt.Sprintf("Error: %s", status.Convert(err).Message()), + output, + ) + } + + user := users.GetUsers()[0] + confirm := false force, _ := cmd.Flags().GetBool("force") if !force { prompt := &survey.Confirm{ Message: fmt.Sprintf( - "Do you want to remove the user '%s' and any associated preauthkeys?", - userName, + "Do you want to remove the user %q (%d) and any associated preauthkeys?", + user.GetName(), user.GetId(), ), } err := survey.AskOne(prompt, &confirm) @@ -118,7 +153,7 @@ var destroyUserCmd = &cobra.Command{ } if confirm || force { - request := &v1.DeleteUserRequest{Name: userName} + request := &v1.DeleteUserRequest{Id: user.GetId()} response, err := client.DeleteUser(ctx, request) if err != nil { @@ -151,6 +186,23 @@ var listUsersCmd = &cobra.Command{ request := &v1.ListUsersRequest{} + id, _ := cmd.Flags().GetInt64("identifier") + username, _ := cmd.Flags().GetString("name") + email, _ := cmd.Flags().GetString("email") + + // filter by one param at most + switch { + case id > 0: + request.Id = uint64(id) + break + case username != "": + request.Name = username + break + case email != "": + request.Email = email + break + } + response, err := client.ListUsers(ctx, request) if err != nil { ErrorOutput( @@ -169,7 +221,7 @@ var listUsersCmd = &cobra.Command{ tableData = append( tableData, []string{ - user.GetId(), + fmt.Sprintf("%d", user.GetId()), user.GetDisplayName(), user.GetName(), user.GetEmail(), @@ -189,17 +241,9 @@ var listUsersCmd = &cobra.Command{ } var renameUserCmd = &cobra.Command{ - Use: "rename OLD_NAME NEW_NAME", + Use: "rename", Short: "Renames a user", Aliases: []string{"mv"}, - Args: func(cmd *cobra.Command, args []string) error { - expectedArguments := 2 - if len(args) < expectedArguments { - return errMissingParameter - } - - return nil - }, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") @@ -207,12 +251,38 @@ var renameUserCmd = &cobra.Command{ defer cancel() defer conn.Close() - request := &v1.RenameUserRequest{ - OldName: args[0], - NewName: args[1], + id, username := usernameAndIDFromFlag(cmd) + listReq := &v1.ListUsersRequest{ + Name: username, + Id: id, } - response, err := client.RenameUser(ctx, request) + users, err := client.ListUsers(ctx, listReq) + if err != nil { + ErrorOutput( + err, + fmt.Sprintf("Error: %s", status.Convert(err).Message()), + output, + ) + } + + if len(users.GetUsers()) != 1 { + err := fmt.Errorf("Unable to determine user to delete, query returned multiple users, use ID") + ErrorOutput( + err, + fmt.Sprintf("Error: %s", status.Convert(err).Message()), + output, + ) + } + + newName, _ := cmd.Flags().GetString("new-name") + + renameReq := &v1.RenameUserRequest{ + OldId: id, + NewName: newName, + } + + response, err := client.RenameUser(ctx, renameReq) if err != nil { ErrorOutput( err, diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 3e9fcb5e..607ebdc7 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -36,18 +36,6 @@ func newHeadscaleV1APIServer(h *Headscale) v1.HeadscaleServiceServer { } } -func (api headscaleV1APIServer) GetUser( - ctx context.Context, - request *v1.GetUserRequest, -) (*v1.GetUserResponse, error) { - user, err := api.h.db.GetUserByName(request.GetName()) - if err != nil { - return nil, err - } - - return &v1.GetUserResponse{User: user.Proto()}, nil -} - func (api headscaleV1APIServer) CreateUser( ctx context.Context, request *v1.CreateUserRequest, @@ -69,7 +57,7 @@ func (api headscaleV1APIServer) RenameUser( ctx context.Context, request *v1.RenameUserRequest, ) (*v1.RenameUserResponse, error) { - oldUser, err := api.h.db.GetUserByName(request.GetOldName()) + oldUser, err := api.h.db.GetUserByID(types.UserID(request.GetOldId())) if err != nil { return nil, err } @@ -91,7 +79,7 @@ func (api headscaleV1APIServer) DeleteUser( ctx context.Context, request *v1.DeleteUserRequest, ) (*v1.DeleteUserResponse, error) { - user, err := api.h.db.GetUserByName(request.GetName()) + user, err := api.h.db.GetUserByID(types.UserID(request.GetId())) if err != nil { return nil, err } @@ -113,7 +101,19 @@ func (api headscaleV1APIServer) ListUsers( ctx context.Context, request *v1.ListUsersRequest, ) (*v1.ListUsersResponse, error) { - users, err := api.h.db.ListUsers() + var err error + var users []types.User + + switch { + case request.GetName() != "": + users, err = api.h.db.ListUsers(&types.User{Name: request.GetName()}) + case request.GetEmail() != "": + users, err = api.h.db.ListUsers(&types.User{Email: request.GetEmail()}) + case request.GetId() != 0: + users, err = api.h.db.ListUsers(&types.User{Model: gorm.Model{ID: uint(request.GetId())}}) + default: + users, err = api.h.db.ListUsers() + } if err != nil { return nil, err } @@ -127,8 +127,6 @@ func (api headscaleV1APIServer) ListUsers( return response[i].Id < response[j].Id }) - log.Trace().Caller().Interface("users", response).Msg("") - return &v1.ListUsersResponse{Users: response}, nil } diff --git a/hscontrol/types/users.go b/hscontrol/types/users.go index 60fbbeda..d2b86ff4 100644 --- a/hscontrol/types/users.go +++ b/hscontrol/types/users.go @@ -108,7 +108,7 @@ func (u *User) TailscaleUserProfile() tailcfg.UserProfile { func (u *User) Proto() *v1.User { return &v1.User{ - Id: strconv.FormatUint(uint64(u.ID), util.Base10), + Id: uint64(u.ID), Name: u.Name, CreatedAt: timestamppb.New(u.CreatedAt), DisplayName: u.DisplayName, diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index e0a61401..54aa05fb 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -130,22 +130,22 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { want := []v1.User{ { - Id: "1", + Id: 1, Name: "user1", }, { - Id: "2", + Id: 2, Name: "user1", Email: "user1@headscale.net", Provider: "oidc", ProviderId: oidcConfig.Issuer + "/user1", }, { - Id: "3", + Id: 3, Name: "user2", }, { - Id: "4", + Id: 4, Name: "user2", Email: "", // Unverified Provider: "oidc", @@ -260,22 +260,22 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: "1", + Id: 1, Name: "user1", }, { - Id: "2", + Id: 2, Name: "user1", Email: "user1@headscale.net", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "3", + Id: 3, Name: "user2", }, { - Id: "4", + Id: 4, Name: "user2", Email: "user2@headscale.net", Provider: "oidc", @@ -295,21 +295,21 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: "1", + Id: 1, Name: "user1", }, { - Id: "2", + Id: 2, Name: "user1", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "3", + Id: 3, Name: "user2", }, { - Id: "4", + Id: 4, Name: "user2", Provider: "oidc", ProviderId: iss + "/user2", @@ -329,14 +329,14 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: "1", + Id: 1, Name: "user1", Email: "user1@headscale.net", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "2", + Id: 2, Name: "user2", Email: "user2@headscale.net", Provider: "oidc", @@ -357,21 +357,21 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: "1", + Id: 1, Name: "user1", }, { - Id: "2", + Id: 2, Name: "user1", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "3", + Id: 3, Name: "user2", }, { - Id: "4", + Id: 4, Name: "user2", Provider: "oidc", ProviderId: iss + "/user2", @@ -393,14 +393,14 @@ func TestOIDC024UserCreation(t *testing.T) { // Hmm I think we will have to overwrite the initial name here // createuser with "user1.headscale.net", but oidc with "user1" { - Id: "1", + Id: 1, Name: "user1", Email: "user1@headscale.net", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "2", + Id: 2, Name: "user2", Email: "user2@headscale.net", Provider: "oidc", @@ -421,21 +421,21 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: "1", + Id: 1, Name: "user1.headscale.net", }, { - Id: "2", + Id: 2, Name: "user1", Provider: "oidc", ProviderId: iss + "/user1", }, { - Id: "3", + Id: 3, Name: "user2.headscale.net", }, { - Id: "4", + Id: 4, Name: "user2", Provider: "oidc", ProviderId: iss + "/user2",