From 770f3dcb9334adac650276dcec90cd980af53c6e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 19 Dec 2024 13:10:10 +0100 Subject: [PATCH] fix tags not resolving to username if email is present (#2309) * ensure valid tags is populated on user gets too Signed-off-by: Kristoffer Dalby * ensure forced tags are added Signed-off-by: Kristoffer Dalby * remove unused envvar in test Signed-off-by: Kristoffer Dalby * debug log auth/unauth tags in policy man Signed-off-by: Kristoffer Dalby * defer shutdown in tags test Signed-off-by: Kristoffer Dalby * add tag test with groups Signed-off-by: Kristoffer Dalby * add email, display name, picture to create user Updates #2166 Signed-off-by: Kristoffer Dalby * add ability to set display and email to cli Signed-off-by: Kristoffer Dalby * add email to test users in integration Signed-off-by: Kristoffer Dalby * fix issue where tags were only assigned to email, not username Fixes #2300 Fixes #2307 Signed-off-by: Kristoffer Dalby * expand principles to correct login name and if fix an issue where nodeip principles might not expand to all relevant IPs instead of taking the first in a prefix. Signed-off-by: Kristoffer Dalby * fix ssh unit test Signed-off-by: Kristoffer Dalby * update cli and oauth tests for users with email Signed-off-by: Kristoffer Dalby * index by test email Signed-off-by: Kristoffer Dalby * fix last test Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- cmd/headscale/cli/nodes.go | 16 +- cmd/headscale/cli/routes.go | 6 +- cmd/headscale/cli/users.go | 26 +++ gen/go/headscale/v1/apikey.pb.go | 2 +- gen/go/headscale/v1/device.pb.go | 2 +- gen/go/headscale/v1/headscale.pb.go | 2 +- gen/go/headscale/v1/node.pb.go | 2 +- gen/go/headscale/v1/policy.pb.go | 2 +- gen/go/headscale/v1/preauthkey.pb.go | 2 +- gen/go/headscale/v1/routes.pb.go | 2 +- gen/go/headscale/v1/user.pb.go | 91 ++++++---- .../headscale/v1/headscale.swagger.json | 9 + hscontrol/db/db_test.go | 8 +- hscontrol/db/node_test.go | 24 +-- hscontrol/db/preauth_keys_test.go | 18 +- hscontrol/db/routes_test.go | 8 +- hscontrol/db/users.go | 11 +- hscontrol/db/users_test.go | 14 +- hscontrol/grpcv1.go | 36 ++-- hscontrol/policy/acls.go | 162 +++++++++++++----- hscontrol/policy/acls_test.go | 50 +++--- hscontrol/policy/pm.go | 4 +- integration/acl_test.go | 26 +-- integration/auth_oidc_test.go | 50 +++--- integration/cli_test.go | 54 +++++- integration/hsic/hsic.go | 2 +- integration/ssh_test.go | 3 - proto/headscale/v1/user.proto | 7 +- 28 files changed, 409 insertions(+), 230 deletions(-) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index b9e97a33..8ffc85f6 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -39,33 +39,33 @@ func init() { err := registerNodeCmd.MarkFlagRequired("user") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } registerNodeCmd.Flags().StringP("key", "k", "", "Key") err = registerNodeCmd.MarkFlagRequired("key") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } nodeCmd.AddCommand(registerNodeCmd) expireNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)") err = expireNodeCmd.MarkFlagRequired("identifier") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } nodeCmd.AddCommand(expireNodeCmd) renameNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)") err = renameNodeCmd.MarkFlagRequired("identifier") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } nodeCmd.AddCommand(renameNodeCmd) deleteNodeCmd.Flags().Uint64P("identifier", "i", 0, "Node identifier (ID)") err = deleteNodeCmd.MarkFlagRequired("identifier") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } nodeCmd.AddCommand(deleteNodeCmd) @@ -73,7 +73,7 @@ func init() { err = moveNodeCmd.MarkFlagRequired("identifier") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } moveNodeCmd.Flags().StringP("user", "u", "", "New user") @@ -85,7 +85,7 @@ func init() { err = moveNodeCmd.MarkFlagRequired("user") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } nodeCmd.AddCommand(moveNodeCmd) @@ -93,7 +93,7 @@ func init() { err = tagCmd.MarkFlagRequired("identifier") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } tagCmd.Flags(). StringSliceP("tags", "t", []string{}, "List of tags to add to the node") diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index dfbcb8fa..e39b407f 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -25,21 +25,21 @@ func init() { enableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") err := enableRouteCmd.MarkFlagRequired("route") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } routesCmd.AddCommand(enableRouteCmd) disableRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") err = disableRouteCmd.MarkFlagRequired("route") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } routesCmd.AddCommand(disableRouteCmd) deleteRouteCmd.Flags().Uint64P("route", "r", 0, "Route identifier (ID)") err = deleteRouteCmd.MarkFlagRequired("route") if err != nil { - log.Fatalf(err.Error()) + log.Fatal(err.Error()) } routesCmd.AddCommand(deleteRouteCmd) } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index 4032b82d..b5f1bc49 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -3,6 +3,7 @@ package cli import ( "errors" "fmt" + "net/url" survey "github.com/AlecAivazis/survey/v2" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" @@ -40,6 +41,9 @@ func usernameAndIDFromFlag(cmd *cobra.Command) (uint64, string) { func init() { rootCmd.AddCommand(userCmd) userCmd.AddCommand(createUserCmd) + createUserCmd.Flags().StringP("display-name", "d", "", "Display name") + createUserCmd.Flags().StringP("email", "e", "", "Email") + createUserCmd.Flags().StringP("picture-url", "p", "", "Profile picture URL") userCmd.AddCommand(listUsersCmd) usernameAndIDFlag(listUsersCmd) listUsersCmd.Flags().StringP("email", "e", "", "Email") @@ -83,6 +87,28 @@ var createUserCmd = &cobra.Command{ request := &v1.CreateUserRequest{Name: userName} + if displayName, _ := cmd.Flags().GetString("display-name"); displayName != "" { + request.DisplayName = displayName + } + + if email, _ := cmd.Flags().GetString("email"); email != "" { + request.Email = email + } + + if pictureURL, _ := cmd.Flags().GetString("picture-url"); pictureURL != "" { + if _, err := url.Parse(pictureURL); err != nil { + ErrorOutput( + err, + fmt.Sprintf( + "Invalid Picture URL: %s", + err, + ), + output, + ) + } + request.PictureUrl = pictureURL + } + log.Trace().Interface("request", request).Msg("Sending CreateUser request") response, err := client.CreateUser(ctx, request) if err != nil { diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index 4c28a3b1..c1529c17 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/apikey.proto diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index b17bda09..de59736b 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/device.proto diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index 7ff023b9..32e97ee6 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/headscale.proto diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index 99045e16..074310e5 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/node.proto diff --git a/gen/go/headscale/v1/policy.pb.go b/gen/go/headscale/v1/policy.pb.go index 957c62cf..ca169b8a 100644 --- a/gen/go/headscale/v1/policy.pb.go +++ b/gen/go/headscale/v1/policy.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/policy.proto diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 2802e7a5..4aef49b0 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/preauthkey.proto diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index 9582527f..dea86494 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/routes.proto diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index d1bf6e7c..9b44d3d3 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc (unknown) // source: headscale/v1/user.proto @@ -127,7 +127,10 @@ type CreateUserRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + PictureUrl string `protobuf:"bytes,4,opt,name=picture_url,json=pictureUrl,proto3" json:"picture_url,omitempty"` } func (x *CreateUserRequest) Reset() { @@ -167,6 +170,27 @@ func (x *CreateUserRequest) GetName() string { return "" } +func (x *CreateUserRequest) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *CreateUserRequest) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *CreateUserRequest) GetPictureUrl() string { + if x != nil { + return x.PictureUrl + } + return "" +} + type CreateUserResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -520,38 +544,43 @@ var file_headscale_v1_user_proto_rawDesc = []byte{ 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x55, 0x72, 0x6c, 0x22, 0x27, - 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x0d, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x55, 0x72, 0x6c, 0x22, 0x81, + 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, + 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, + 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x75, 0x72, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x55, + 0x72, 0x6c, 0x22, 0x3c, 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x6c, 0x64, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6f, 0x6c, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, + 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, - 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x45, 0x0a, 0x11, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, - 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x15, 0x0a, 0x06, 0x6f, 0x6c, - 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6f, 0x6c, 0x64, 0x49, - 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x65, 0x77, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x65, 0x77, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3c, 0x0a, 0x12, - 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22, - 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, - 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, - 0x61, 0x69, 0x6c, 0x22, 0x3d, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, - 0x72, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, - 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x22, 0x14, 0x0a, 0x12, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x4c, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x3d, + 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x05, 0x75, 0x73, 0x65, 0x72, 0x73, 0x42, 0x29, 0x5a, + 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, + 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, + 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 1f0a9c4a..f6813391 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -1039,6 +1039,15 @@ "properties": { "name": { "type": "string" + }, + "displayName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "pictureUrl": { + "type": "string" } } }, diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go index 95c82160..c3d9a835 100644 --- a/hscontrol/db/db_test.go +++ b/hscontrol/db/db_test.go @@ -278,9 +278,9 @@ func TestConstraints(t *testing.T) { { name: "no-duplicate-username-if-no-oidc", run: func(t *testing.T, db *gorm.DB) { - _, err := CreateUser(db, "user1") + _, err := CreateUser(db, types.User{Name: "user1"}) require.NoError(t, err) - _, err = CreateUser(db, "user1") + _, err = CreateUser(db, types.User{Name: "user1"}) requireConstraintFailed(t, err) }, }, @@ -331,7 +331,7 @@ func TestConstraints(t *testing.T) { { name: "allow-duplicate-username-cli-then-oidc", run: func(t *testing.T, db *gorm.DB) { - _, err := CreateUser(db, "user1") // Create CLI username + _, err := CreateUser(db, types.User{Name: "user1"}) // Create CLI username require.NoError(t, err) user := types.User{ @@ -354,7 +354,7 @@ func TestConstraints(t *testing.T) { err := db.Save(&user).Error require.NoError(t, err) - _, err = CreateUser(db, "user1") // Create CLI username + _, err = CreateUser(db, types.User{Name: "user1"}) // Create CLI username require.NoError(t, err) }, }, diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 7c83c1be..270fd91b 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -27,7 +27,7 @@ import ( ) func (s *Suite) TestGetNode(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -56,7 +56,7 @@ func (s *Suite) TestGetNode(c *check.C) { } func (s *Suite) TestGetNodeByID(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -85,7 +85,7 @@ func (s *Suite) TestGetNodeByID(c *check.C) { } func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -116,7 +116,7 @@ func (s *Suite) TestGetNodeByAnyNodeKey(c *check.C) { } func (s *Suite) TestHardDeleteNode(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) nodeKey := key.NewNode() @@ -141,7 +141,7 @@ func (s *Suite) TestHardDeleteNode(c *check.C) { } func (s *Suite) TestListPeers(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -188,7 +188,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { stor := make([]base, 0) for _, name := range []string{"test", "admin"} { - user, err := db.CreateUser(name) + user, err := db.CreateUser(types.User{Name: name}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) c.Assert(err, check.IsNil) @@ -279,7 +279,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) { } func (s *Suite) TestExpireNode(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -320,7 +320,7 @@ func (s *Suite) TestExpireNode(c *check.C) { } func (s *Suite) TestSetTags(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -565,7 +565,7 @@ func TestAutoApproveRoutes(t *testing.T) { require.NoError(t, err) require.NotNil(t, pol) - user, err := adb.CreateUser("test") + user, err := adb.CreateUser(types.User{Name: "test"}) require.NoError(t, err) pak, err := adb.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -706,7 +706,7 @@ func TestListEphemeralNodes(t *testing.T) { t.Fatalf("creating db: %s", err) } - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) require.NoError(t, err) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -762,10 +762,10 @@ func TestRenameNode(t *testing.T) { t.Fatalf("creating db: %s", err) } - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) require.NoError(t, err) - user2, err := db.CreateUser("test2") + user2, err := db.CreateUser(types.User{Name: "user2"}) require.NoError(t, err) node := types.Node{ diff --git a/hscontrol/db/preauth_keys_test.go b/hscontrol/db/preauth_keys_test.go index 3c56a35e..a3a24ac7 100644 --- a/hscontrol/db/preauth_keys_test.go +++ b/hscontrol/db/preauth_keys_test.go @@ -15,7 +15,7 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) { _, err := db.CreatePreAuthKey(12345, true, false, nil, nil) c.Assert(err, check.NotNil) - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) key, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil) @@ -41,7 +41,7 @@ func (*Suite) TestCreatePreAuthKey(c *check.C) { } func (*Suite) TestExpiredPreAuthKey(c *check.C) { - user, err := db.CreateUser("test2") + user, err := db.CreateUser(types.User{Name: "test2"}) c.Assert(err, check.IsNil) now := time.Now().Add(-5 * time.Second) @@ -60,7 +60,7 @@ func (*Suite) TestPreAuthKeyDoesNotExist(c *check.C) { } func (*Suite) TestValidateKeyOk(c *check.C) { - user, err := db.CreateUser("test3") + user, err := db.CreateUser(types.User{Name: "test3"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil) @@ -72,7 +72,7 @@ func (*Suite) TestValidateKeyOk(c *check.C) { } func (*Suite) TestAlreadyUsedKey(c *check.C) { - user, err := db.CreateUser("test4") + user, err := db.CreateUser(types.User{Name: "test4"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -94,7 +94,7 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) { } func (*Suite) TestReusableBeingUsedKey(c *check.C) { - user, err := db.CreateUser("test5") + user, err := db.CreateUser(types.User{Name: "test5"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil) @@ -116,7 +116,7 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) { } func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) { - user, err := db.CreateUser("test6") + user, err := db.CreateUser(types.User{Name: "test6"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -128,7 +128,7 @@ func (*Suite) TestNotReusableNotBeingUsedKey(c *check.C) { } func (*Suite) TestExpirePreauthKey(c *check.C) { - user, err := db.CreateUser("test3") + user, err := db.CreateUser(types.User{Name: "test3"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), true, false, nil, nil) @@ -145,7 +145,7 @@ func (*Suite) TestExpirePreauthKey(c *check.C) { } func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) { - user, err := db.CreateUser("test6") + user, err := db.CreateUser(types.User{Name: "test6"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -158,7 +158,7 @@ func (*Suite) TestNotReusableMarkedAsUsed(c *check.C) { } func (*Suite) TestPreAuthKeyACLTags(c *check.C) { - user, err := db.CreateUser("test8") + user, err := db.CreateUser(types.User{Name: "test8"}) c.Assert(err, check.IsNil) _, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, []string{"badtag"}) diff --git a/hscontrol/db/routes_test.go b/hscontrol/db/routes_test.go index ed9d4c04..909024fc 100644 --- a/hscontrol/db/routes_test.go +++ b/hscontrol/db/routes_test.go @@ -32,7 +32,7 @@ var mp = func(p string) netip.Prefix { } func (s *Suite) TestGetRoutes(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -76,7 +76,7 @@ func (s *Suite) TestGetRoutes(c *check.C) { } func (s *Suite) TestGetEnableRoutes(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -150,7 +150,7 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) { } func (s *Suite) TestIsUniquePrefix(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -231,7 +231,7 @@ func (s *Suite) TestIsUniquePrefix(c *check.C) { } func (s *Suite) TestDeleteRoutes(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) diff --git a/hscontrol/db/users.go b/hscontrol/db/users.go index 0eaa9ea3..3fdc14a0 100644 --- a/hscontrol/db/users.go +++ b/hscontrol/db/users.go @@ -15,22 +15,19 @@ var ( ErrUserStillHasNodes = errors.New("user not empty: node(s) found") ) -func (hsdb *HSDatabase) CreateUser(name string) (*types.User, error) { +func (hsdb *HSDatabase) CreateUser(user types.User) (*types.User, error) { return Write(hsdb.DB, func(tx *gorm.DB) (*types.User, error) { - return CreateUser(tx, name) + return CreateUser(tx, user) }) } // CreateUser creates a new User. Returns error if could not be created // or another user already exists. -func CreateUser(tx *gorm.DB, name string) (*types.User, error) { - err := util.CheckForFQDNRules(name) +func CreateUser(tx *gorm.DB, user types.User) (*types.User, error) { + err := util.CheckForFQDNRules(user.Name) if err != nil { return nil, err } - user := types.User{ - Name: name, - } if err := tx.Create(&user).Error; err != nil { return nil, fmt.Errorf("creating user: %w", err) } diff --git a/hscontrol/db/users_test.go b/hscontrol/db/users_test.go index 06073762..6cec2d5a 100644 --- a/hscontrol/db/users_test.go +++ b/hscontrol/db/users_test.go @@ -11,7 +11,7 @@ import ( ) func (s *Suite) TestCreateAndDestroyUser(c *check.C) { - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) c.Assert(user.Name, check.Equals, "test") @@ -30,7 +30,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) { err := db.DestroyUser(9998) c.Assert(err, check.Equals, ErrUserNotFound) - user, err := db.CreateUser("test") + user, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -43,7 +43,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) { // destroying a user also deletes all associated preauthkeys c.Assert(result.Error, check.Equals, gorm.ErrRecordNotFound) - user, err = db.CreateUser("test") + user, err = db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) pak, err = db.CreatePreAuthKey(types.UserID(user.ID), false, false, nil, nil) @@ -64,7 +64,7 @@ func (s *Suite) TestDestroyUserErrors(c *check.C) { } func (s *Suite) TestRenameUser(c *check.C) { - userTest, err := db.CreateUser("test") + userTest, err := db.CreateUser(types.User{Name: "test"}) c.Assert(err, check.IsNil) c.Assert(userTest.Name, check.Equals, "test") @@ -86,7 +86,7 @@ func (s *Suite) TestRenameUser(c *check.C) { err = db.RenameUser(99988, "test") c.Assert(err, check.Equals, ErrUserNotFound) - userTest2, err := db.CreateUser("test2") + userTest2, err := db.CreateUser(types.User{Name: "test2"}) c.Assert(err, check.IsNil) c.Assert(userTest2.Name, check.Equals, "test2") @@ -98,10 +98,10 @@ func (s *Suite) TestRenameUser(c *check.C) { } func (s *Suite) TestSetMachineUser(c *check.C) { - oldUser, err := db.CreateUser("old") + oldUser, err := db.CreateUser(types.User{Name: "old"}) c.Assert(err, check.IsNil) - newUser, err := db.CreateUser("new") + newUser, err := db.CreateUser(types.User{Name: "new"}) c.Assert(err, check.IsNil) pak, err := db.CreatePreAuthKey(types.UserID(oldUser.ID), false, false, nil, nil) diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 607ebdc7..b7c7e50e 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -11,7 +11,9 @@ import ( "strings" "time" + "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" + "github.com/samber/lo" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" @@ -21,6 +23,7 @@ import ( v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/db" + "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" ) @@ -40,7 +43,13 @@ func (api headscaleV1APIServer) CreateUser( ctx context.Context, request *v1.CreateUserRequest, ) (*v1.CreateUserResponse, error) { - user, err := api.h.db.CreateUser(request.GetName()) + newUser := types.User{ + Name: request.GetName(), + DisplayName: request.GetDisplayName(), + Email: request.GetEmail(), + ProfilePicURL: request.GetPictureUrl(), + } + user, err := api.h.db.CreateUser(newUser) if err != nil { return nil, err } @@ -457,19 +466,7 @@ func (api headscaleV1APIServer) ListNodes( return nil, err } - response := make([]*v1.Node, len(nodes)) - for index, node := range nodes { - resp := node.Proto() - - // Populate the online field based on - // currently connected nodes. - if val, ok := isLikelyConnected.Load(node.ID); ok && val { - resp.Online = true - } - - response[index] = resp - } - + response := nodesToProto(api.h.polMan, isLikelyConnected, nodes) return &v1.ListNodesResponse{Nodes: response}, nil } @@ -482,6 +479,11 @@ func (api headscaleV1APIServer) ListNodes( return nodes[i].ID < nodes[j].ID }) + response := nodesToProto(api.h.polMan, isLikelyConnected, nodes) + return &v1.ListNodesResponse{Nodes: response}, nil +} + +func nodesToProto(polMan policy.PolicyManager, isLikelyConnected *xsync.MapOf[types.NodeID, bool], nodes types.Nodes) []*v1.Node { response := make([]*v1.Node, len(nodes)) for index, node := range nodes { resp := node.Proto() @@ -492,12 +494,12 @@ func (api headscaleV1APIServer) ListNodes( resp.Online = true } - validTags := api.h.polMan.Tags(node) - resp.ValidTags = validTags + tags := polMan.Tags(node) + resp.ValidTags = lo.Uniq(append(tags, node.ForcedTags...)) response[index] = resp } - return &v1.ListNodesResponse{Nodes: response}, nil + return response } func (api headscaleV1APIServer) MoveNode( diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 5848ec33..3d7a6f4a 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "iter" "net/netip" "os" "slices" @@ -361,37 +362,67 @@ func (pol *ACLPolicy) CompileSSHPolicy( ) } - principals := make([]*tailcfg.SSHPrincipal, 0, len(sshACL.Sources)) - for innerIndex, rawSrc := range sshACL.Sources { - if isWildcard(rawSrc) { - principals = append(principals, &tailcfg.SSHPrincipal{ + var principals []*tailcfg.SSHPrincipal + for innerIndex, srcToken := range sshACL.Sources { + if isWildcard(srcToken) { + principals = []*tailcfg.SSHPrincipal{{ Any: true, - }) - } else if isGroup(rawSrc) { - users, err := pol.expandUsersFromGroup(rawSrc) + }} + break + } + + // If the token is a group, expand the users and validate + // them. Then use the .Username() to get the login name + // that corresponds with the User info in the netmap. + if isGroup(srcToken) { + usersFromGroup, err := pol.expandUsersFromGroup(srcToken) if err != nil { return nil, fmt.Errorf("parsing SSH policy, expanding user from group, index: %d->%d: %w", index, innerIndex, err) } - for _, user := range users { + for _, userStr := range usersFromGroup { + user, err := findUserFromTokenOrErr(users, userStr) + if err != nil { + log.Trace().Err(err).Msg("user not found") + continue + } + principals = append(principals, &tailcfg.SSHPrincipal{ - UserLogin: user, - }) - } - } else { - expandedSrcs, err := pol.ExpandAlias( - peers, - users, - rawSrc, - ) - if err != nil { - return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err) - } - for _, expandedSrc := range expandedSrcs.Prefixes() { - principals = append(principals, &tailcfg.SSHPrincipal{ - NodeIP: expandedSrc.Addr().String(), + UserLogin: user.Username(), }) } + + continue + } + + // Try to check if the token is a user, if it is, then we + // can use the .Username() to get the login name that + // corresponds with the User info in the netmap. + // TODO(kradalby): This is a bit of a hack, and it should go + // away with the new policy where users can be reliably determined. + if user, err := findUserFromTokenOrErr(users, srcToken); err == nil { + principals = append(principals, &tailcfg.SSHPrincipal{ + UserLogin: user.Username(), + }) + continue + } + + // This is kind of then non-ideal scenario where we dont really know + // what to do with the token, so we expand it to IP addresses of nodes. + // The pro here is that we have a pretty good lockdown on the mapping + // between users and node, but it can explode if a user owns many nodes. + ips, err := pol.ExpandAlias( + peers, + users, + srcToken, + ) + if err != nil { + return nil, fmt.Errorf("parsing SSH policy, expanding alias, index: %d->%d: %w", index, innerIndex, err) + } + for addr := range ipSetAll(ips) { + principals = append(principals, &tailcfg.SSHPrincipal{ + NodeIP: addr.String(), + }) } } @@ -411,6 +442,19 @@ func (pol *ACLPolicy) CompileSSHPolicy( }, nil } +// ipSetAll returns a function that iterates over all the IPs in the IPSet. +func ipSetAll(ipSet *netipx.IPSet) iter.Seq[netip.Addr] { + return func(yield func(netip.Addr) bool) { + for _, rng := range ipSet.Ranges() { + for ip := rng.From(); ip.Compare(rng.To()) <= 0; ip = ip.Next() { + if !yield(ip) { + return + } + } + } + } +} + func sshCheckAction(duration string) (*tailcfg.SSHAction, error) { sessionLength, err := time.ParseDuration(duration) if err != nil { @@ -934,6 +978,7 @@ func isAutoGroup(str string) bool { // Invalid tags are tags added by a user on a node, and that user doesn't have authority to add this tag. // Valid tags are tags added by a user that is allowed in the ACL policy to add this tag. func (pol *ACLPolicy) TagsOfNode( + users []types.User, node *types.Node, ) ([]string, []string) { var validTags []string @@ -956,7 +1001,12 @@ func (pol *ACLPolicy) TagsOfNode( } var found bool for _, owner := range owners { - if node.User.Username() == owner { + user, err := findUserFromTokenOrErr(users, owner) + if err != nil { + log.Trace().Caller().Err(err).Msg("could not determine user to filter tags by") + } + + if node.User.ID == user.ID { found = true } } @@ -988,30 +1038,12 @@ func (pol *ACLPolicy) TagsOfNode( func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string) types.Nodes { var out types.Nodes - var potentialUsers []types.User - for _, user := range users { - if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == userToken { - // If a user is matching with a known unique field, - // disgard all other users and only keep the current - // user. - potentialUsers = []types.User{user} - - break - } - if user.Email == userToken { - potentialUsers = append(potentialUsers, user) - } - if user.Name == userToken { - potentialUsers = append(potentialUsers, user) - } + user, err := findUserFromTokenOrErr(users, userToken) + if err != nil { + log.Trace().Caller().Err(err).Msg("could not determine user to filter nodes by") + return out } - if len(potentialUsers) != 1 { - return nil - } - - user := potentialUsers[0] - for _, node := range nodes { if node.User.ID == user.ID { out = append(out, node) @@ -1021,6 +1053,44 @@ func filterNodesByUser(nodes types.Nodes, users []types.User, userToken string) return out } +var ( + ErrorNoUserMatching = errors.New("no user matching") + ErrorMultipleUserMatching = errors.New("multiple users matching") +) + +func findUserFromTokenOrErr( + users []types.User, + token string, +) (types.User, error) { + var potentialUsers []types.User + for _, user := range users { + if user.ProviderIdentifier.Valid && user.ProviderIdentifier.String == token { + // If a user is matching with a known unique field, + // disgard all other users and only keep the current + // user. + potentialUsers = []types.User{user} + + break + } + if user.Email == token { + potentialUsers = append(potentialUsers, user) + } + if user.Name == token { + potentialUsers = append(potentialUsers, user) + } + } + + if len(potentialUsers) == 0 { + return types.User{}, fmt.Errorf("user with token %q not found: %w", token, ErrorNoUserMatching) + } + + if len(potentialUsers) > 1 { + return types.User{}, fmt.Errorf("multiple users with token %q found: %w", token, ErrorNoUserMatching) + } + + return potentialUsers[0], nil +} + // FilterNodesByACL returns the list of peers authorized to be accessed from a given node. func FilterNodesByACL( node *types.Node, diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index b00cec12..ae8898bf 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -2735,6 +2735,12 @@ func TestReduceFilterRules(t *testing.T) { } func Test_getTags(t *testing.T) { + users := []types.User{ + { + Model: gorm.Model{ID: 1}, + Name: "joe", + }, + } type args struct { aclPolicy *ACLPolicy node *types.Node @@ -2754,9 +2760,7 @@ func Test_getTags(t *testing.T) { }, }, node: &types.Node{ - User: types.User{ - Name: "joe", - }, + User: users[0], Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:valid"}, }, @@ -2774,9 +2778,7 @@ func Test_getTags(t *testing.T) { }, }, node: &types.Node{ - User: types.User{ - Name: "joe", - }, + User: users[0], Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:valid", "tag:invalid"}, }, @@ -2794,9 +2796,7 @@ func Test_getTags(t *testing.T) { }, }, node: &types.Node{ - User: types.User{ - Name: "joe", - }, + User: users[0], Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{ "tag:invalid", @@ -2818,9 +2818,7 @@ func Test_getTags(t *testing.T) { }, }, node: &types.Node{ - User: types.User{ - Name: "joe", - }, + User: users[0], Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:invalid", "very-invalid"}, }, @@ -2834,9 +2832,7 @@ func Test_getTags(t *testing.T) { args: args{ aclPolicy: &ACLPolicy{}, node: &types.Node{ - User: types.User{ - Name: "joe", - }, + User: users[0], Hostinfo: &tailcfg.Hostinfo{ RequestTags: []string{"tag:invalid", "very-invalid"}, }, @@ -2849,6 +2845,7 @@ func Test_getTags(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { gotValid, gotInvalid := test.args.aclPolicy.TagsOfNode( + users, test.args.node, ) for _, valid := range gotValid { @@ -3542,6 +3539,11 @@ func Test_getFilteredByACLPeers(t *testing.T) { } func TestSSHRules(t *testing.T) { + users := []types.User{ + { + Name: "user1", + }, + } tests := []struct { name string node types.Node @@ -3555,18 +3557,14 @@ func TestSSHRules(t *testing.T) { Hostname: "testnodes", IPv4: iap("100.64.99.42"), UserID: 0, - User: types.User{ - Name: "user1", - }, + User: users[0], }, peers: types.Nodes{ &types.Node{ Hostname: "testnodes2", IPv4: iap("100.64.0.1"), UserID: 0, - User: types.User{ - Name: "user1", - }, + User: users[0], }, }, pol: ACLPolicy{ @@ -3679,18 +3677,14 @@ func TestSSHRules(t *testing.T) { Hostname: "testnodes", IPv4: iap("100.64.0.1"), UserID: 0, - User: types.User{ - Name: "user1", - }, + User: users[0], }, peers: types.Nodes{ &types.Node{ Hostname: "testnodes2", IPv4: iap("100.64.99.42"), UserID: 0, - User: types.User{ - Name: "user1", - }, + User: users[0], }, }, pol: ACLPolicy{ @@ -3728,7 +3722,7 @@ func TestSSHRules(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := tt.pol.CompileSSHPolicy(&tt.node, []types.User{}, tt.peers) + got, err := tt.pol.CompileSSHPolicy(&tt.node, users, tt.peers) require.NoError(t, err) if diff := cmp.Diff(tt.want, got); diff != "" { diff --git a/hscontrol/policy/pm.go b/hscontrol/policy/pm.go index a9de1aa1..4e10003e 100644 --- a/hscontrol/policy/pm.go +++ b/hscontrol/policy/pm.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/juanfont/headscale/hscontrol/types" + "github.com/rs/zerolog/log" "go4.org/netipx" "tailscale.com/tailcfg" "tailscale.com/util/deephash" @@ -161,7 +162,8 @@ func (pm *PolicyManagerV1) Tags(node *types.Node) []string { return nil } - tags, _ := pm.pol.TagsOfNode(node) + tags, invalid := pm.pol.TagsOfNode(pm.users, node) + log.Debug().Strs("authorised_tags", tags).Strs("unauthorised_tags", invalid).Uint64("node.id", node.ID.Uint64()).Msg("tags provided by policy") return tags } diff --git a/integration/acl_test.go b/integration/acl_test.go index 6606a132..888110ac 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -119,8 +119,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 3, // ns1 + ns2 - "user2": 3, // ns2 + ns1 + "user1@test.no": 3, // ns1 + ns2 + "user2@test.no": 3, // ns2 + ns1 }, }, // Test that when we have two users, which cannot see @@ -145,8 +145,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 1, - "user2": 1, + "user1@test.no": 1, + "user2@test.no": 1, }, }, // Test that when we have two users, with ACLs and they @@ -181,8 +181,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 3, - "user2": 3, + "user1@test.no": 3, + "user2@test.no": 3, }, }, // Test that when we have two users, that are isolated, @@ -213,8 +213,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 3, // ns1 + ns2 - "user2": 3, // ns1 + ns2 (return path) + "user1@test.no": 3, // ns1 + ns2 + "user2@test.no": 3, // ns1 + ns2 (return path) }, }, "very-large-destination-prefix-1372": { @@ -241,8 +241,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 3, // ns1 + ns2 - "user2": 3, // ns1 + ns2 (return path) + "user1@test.no": 3, // ns1 + ns2 + "user2@test.no": 3, // ns1 + ns2 (return path) }, }, "ipv6-acls-1470": { @@ -259,8 +259,8 @@ func TestACLHostsInNetMapTable(t *testing.T) { }, }, }, want: map[string]int{ - "user1": 3, // ns1 + ns2 - "user2": 3, // ns2 + ns1 + "user1@test.no": 3, // ns1 + ns2 + "user2@test.no": 3, // ns2 + ns1 }, }, } @@ -282,7 +282,7 @@ func TestACLHostsInNetMapTable(t *testing.T) { allClients, err := scenario.ListTailscaleClients() require.NoError(t, err) - err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1"]) + err = scenario.WaitForTailscaleSyncWithPeerCount(testCase.want["user1@test.no"]) require.NoError(t, err) for _, client := range allClients { diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index 52d28054..22459876 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -130,8 +130,9 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { want := []v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, { Id: 2, @@ -141,8 +142,9 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { ProviderId: oidcConfig.Issuer + "/user1", }, { - Id: 3, - Name: "user2", + Id: 3, + Name: "user2", + Email: "user2@test.no", }, { Id: 4, @@ -260,8 +262,9 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, { Id: 2, @@ -271,8 +274,9 @@ func TestOIDC024UserCreation(t *testing.T) { ProviderId: iss + "/user1", }, { - Id: 3, - Name: "user2", + Id: 3, + Name: "user2", + Email: "user2@test.no", }, { Id: 4, @@ -295,8 +299,9 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, { Id: 2, @@ -305,8 +310,9 @@ func TestOIDC024UserCreation(t *testing.T) { ProviderId: iss + "/user1", }, { - Id: 3, - Name: "user2", + Id: 3, + Name: "user2", + Email: "user2@test.no", }, { Id: 4, @@ -357,8 +363,9 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, { Id: 2, @@ -367,8 +374,9 @@ func TestOIDC024UserCreation(t *testing.T) { ProviderId: iss + "/user1", }, { - Id: 3, - Name: "user2", + Id: 3, + Name: "user2", + Email: "user2@test.no", }, { Id: 4, @@ -421,8 +429,9 @@ func TestOIDC024UserCreation(t *testing.T) { want: func(iss string) []v1.User { return []v1.User{ { - Id: 1, - Name: "user1.headscale.net", + Id: 1, + Name: "user1.headscale.net", + Email: "user1.headscale.net@test.no", }, { Id: 2, @@ -431,8 +440,9 @@ func TestOIDC024UserCreation(t *testing.T) { ProviderId: iss + "/user1", }, { - Id: 3, - Name: "user2.headscale.net", + Id: 3, + Name: "user2.headscale.net", + Email: "user2.headscale.net@test.no", }, { Id: 4, diff --git a/integration/cli_test.go b/integration/cli_test.go index 1870041b..08d5937c 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -135,8 +135,9 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listByUsername, sortWithID) want := []*v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, } @@ -161,8 +162,9 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listByID, sortWithID) want = []*v1.User{ { - Id: 1, - Name: "user1", + Id: 1, + Name: "user1", + Email: "user1@test.no", }, } @@ -199,8 +201,9 @@ func TestUserCommand(t *testing.T) { slices.SortFunc(listAfterIDDelete, sortWithID) want = []*v1.User{ { - Id: 2, - Name: "newname", + Id: 2, + Name: "newname", + Email: "user2@test.no", }, } @@ -930,7 +933,23 @@ func TestNodeAdvertiseTagCommand(t *testing.T) { wantTag: false, }, { - name: "with-policy", + name: "with-policy-email", + policy: &policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:test": {"user1@test.no"}, + }, + }, + wantTag: true, + }, + { + name: "with-policy-username", policy: &policy.ACLPolicy{ ACLs: []policy.ACL{ { @@ -945,13 +964,32 @@ func TestNodeAdvertiseTagCommand(t *testing.T) { }, wantTag: true, }, + { + name: "with-policy-groups", + policy: &policy.ACLPolicy{ + Groups: policy.Groups{ + "group:admins": []string{"user1"}, + }, + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:test": {"group:admins"}, + }, + }, + wantTag: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - // defer scenario.ShutdownAssertNoPanics(t) + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 1, diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index b2a2701e..883fc8bc 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -702,7 +702,7 @@ func (t *HeadscaleInContainer) WaitForRunning() error { func (t *HeadscaleInContainer) CreateUser( user string, ) error { - command := []string{"headscale", "users", "create", user} + command := []string{"headscale", "users", "create", user, fmt.Sprintf("--email=%s@test.no", user)} _, _, err := dockertestutil.ExecuteCommand( t.container, diff --git a/integration/ssh_test.go b/integration/ssh_test.go index c31cc108..bc67a73e 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -69,9 +69,6 @@ func sshScenario(t *testing.T, policy *policy.ACLPolicy, clientsPerUser int) *Sc }, hsic.WithACLPolicy(policy), hsic.WithTestName("ssh"), - hsic.WithConfigEnv(map[string]string{ - "HEADSCALE_EXPERIMENTAL_FEATURE_SSH": "1", - }), ) assertNoErr(t, err) diff --git a/proto/headscale/v1/user.proto b/proto/headscale/v1/user.proto index 591553dd..bd71bcb1 100644 --- a/proto/headscale/v1/user.proto +++ b/proto/headscale/v1/user.proto @@ -15,7 +15,12 @@ message User { string profile_pic_url = 8; } -message CreateUserRequest { string name = 1; } +message CreateUserRequest { + string name = 1; + string display_name = 2; + string email = 3; + string picture_url = 4; +} message CreateUserResponse { User user = 1; }