diff --git a/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go b/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go index c7984c072d229..85f08816262a8 100644 --- a/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go +++ b/api/gen/proto/go/userpreferences/v1/userpreferences.pb.go @@ -107,9 +107,6 @@ type GetUserPreferencesRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - - // username is the username of the owner of the user preferences to get. - Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` } func (x *GetUserPreferencesRequest) Reset() { @@ -144,13 +141,6 @@ func (*GetUserPreferencesRequest) Descriptor() ([]byte, []int) { return file_teleport_userpreferences_v1_userpreferences_proto_rawDescGZIP(), []int{1} } -func (x *GetUserPreferencesRequest) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - // GetUserPreferencesResponse is a response to get the user preferences. type GetUserPreferencesResponse struct { state protoimpl.MessageState @@ -208,8 +198,6 @@ type UpsertUserPreferencesRequest struct { // preferences is the new user preferences to set. Preferences *UserPreferences `protobuf:"bytes,1,opt,name=preferences,proto3" json:"preferences,omitempty"` - // username is the username of the owner of the user preferences to update. - Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` } func (x *UpsertUserPreferencesRequest) Reset() { @@ -251,13 +239,6 @@ func (x *UpsertUserPreferencesRequest) GetPreferences() *UserPreferences { return nil } -func (x *UpsertUserPreferencesRequest) GetUsername() string { - if x != nil { - return x.Username - } - return "" -} - var File_teleport_userpreferences_v1_userpreferences_proto protoreflect.FileDescriptor var file_teleport_userpreferences_v1_userpreferences_proto_rawDesc = []byte{ @@ -290,49 +271,48 @@ var file_teleport_userpreferences_v1_userpreferences_proto_rawDesc = []byte{ 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x6e, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x07, 0x6f, 0x6e, 0x62, - 0x6f, 0x61, 0x72, 0x64, 0x22, 0x37, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x6f, 0x61, 0x72, 0x64, 0x22, 0x2b, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x6c, 0x0a, - 0x1a, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0b, 0x70, - 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, - 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, - 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x0b, - 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x8a, 0x01, 0x0a, 0x1c, - 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, - 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x0b, - 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, + 0x74, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x22, 0x6c, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4e, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x73, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, + 0x7e, 0x0a, 0x1c, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x4e, 0x0a, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x73, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x4a, + 0x04, 0x08, 0x02, 0x10, 0x03, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, + 0x8c, 0x02, 0x0a, 0x16, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, + 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, + 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, - 0x0b, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x8c, 0x02, 0x0a, 0x16, 0x55, 0x73, 0x65, - 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, - 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, - 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, - 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x75, 0x73, - 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x15, 0x55, - 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, - 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x65, - 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x59, 0x5a, 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x75, 0x73, - 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, - 0x3b, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x59, + 0x5a, 0x57, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, + 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x70, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( diff --git a/api/proto/teleport/userpreferences/v1/userpreferences.proto b/api/proto/teleport/userpreferences/v1/userpreferences.proto index 1d142c9632ee1..229a768f444c7 100644 --- a/api/proto/teleport/userpreferences/v1/userpreferences.proto +++ b/api/proto/teleport/userpreferences/v1/userpreferences.proto @@ -35,8 +35,8 @@ message UserPreferences { // GetUserPreferencesRequest is a request to get the user preferences. message GetUserPreferencesRequest { - // username is the username of the owner of the user preferences to get. - string username = 1; + reserved 1; + reserved "username"; } // GetUserPreferencesResponse is a response to get the user preferences. @@ -49,8 +49,9 @@ message GetUserPreferencesResponse { message UpsertUserPreferencesRequest { // preferences is the new user preferences to set. UserPreferences preferences = 1; - // username is the username of the owner of the user preferences to update. - string username = 2; + + reserved 2; + reserved "username"; } // UserPreferencesService is a service that stores user settings. diff --git a/lib/auth/assist/assistv1/service.go b/lib/auth/assist/assistv1/service.go index a46dda271bbe1..fd5a5b0efb940 100644 --- a/lib/auth/assist/assistv1/service.go +++ b/lib/auth/assist/assistv1/service.go @@ -93,13 +93,31 @@ func NewService(cfg *ServiceConfig) (*Service, error) { // CreateAssistantConversation creates a new conversation entry in the backend. func (a *Service) CreateAssistantConversation(ctx context.Context, req *assist.CreateAssistantConversationRequest) (*assist.CreateAssistantConversationResponse, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbCreate) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to create conversation for user %q", authCtx.User.GetName(), req.Username) + } + resp, err := a.backend.CreateAssistantConversation(ctx, req) return resp, trace.Wrap(err) } // UpdateAssistantConversationInfo updates the conversation info for a conversation. -func (a *Service) UpdateAssistantConversationInfo(ctx context.Context, request *assist.UpdateAssistantConversationInfoRequest) (*emptypb.Empty, error) { - err := a.backend.UpdateAssistantConversationInfo(ctx, request) +func (a *Service) UpdateAssistantConversationInfo(ctx context.Context, req *assist.UpdateAssistantConversationInfoRequest) (*emptypb.Empty, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbUpdate) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to update conversation for user %q", authCtx.User.GetName(), req.Username) + } + + err = a.backend.UpdateAssistantConversationInfo(ctx, req) if err != nil { return &emptypb.Empty{}, trace.Wrap(err) } @@ -109,33 +127,93 @@ func (a *Service) UpdateAssistantConversationInfo(ctx context.Context, request * // GetAssistantConversations returns all conversations started by a user. func (a *Service) GetAssistantConversations(ctx context.Context, req *assist.GetAssistantConversationsRequest) (*assist.GetAssistantConversationsResponse, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbList) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to list conversations for user %q", authCtx.User.GetName(), req.GetUsername()) + } + resp, err := a.backend.GetAssistantConversations(ctx, req) return resp, trace.Wrap(err) } // DeleteAssistantConversation deletes a conversation entry and associated messages from the backend. func (a *Service) DeleteAssistantConversation(ctx context.Context, req *assist.DeleteAssistantConversationRequest) (*emptypb.Empty, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbDelete) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to delete conversation for user %q", authCtx.User.GetName(), req.GetUsername()) + } + return &emptypb.Empty{}, trace.Wrap(a.backend.DeleteAssistantConversation(ctx, req)) } // GetAssistantMessages returns all messages with given conversation ID. func (a *Service) GetAssistantMessages(ctx context.Context, req *assist.GetAssistantMessagesRequest) (*assist.GetAssistantMessagesResponse, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbRead) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to get messages for user %q", authCtx.User.GetName(), req.GetUsername()) + } + resp, err := a.backend.GetAssistantMessages(ctx, req) return resp, trace.Wrap(err) } // CreateAssistantMessage adds the message to the backend. func (a *Service) CreateAssistantMessage(ctx context.Context, req *assist.CreateAssistantMessageRequest) (*emptypb.Empty, error) { + authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindAssistant, types.VerbCreate) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + if userHasAccess(authCtx, req) { + return nil, trace.AccessDenied("user %q is not allowed to create message for user %q", authCtx.User.GetName(), req.GetUsername()) + } + return &emptypb.Empty{}, trace.Wrap(a.backend.CreateAssistantMessage(ctx, req)) } // IsAssistEnabled returns true if the assist is enabled or not on the auth level. func (a *Service) IsAssistEnabled(ctx context.Context, _ *assist.IsAssistEnabledRequest) (*assist.IsAssistEnabledResponse, error) { + // If the embedder is not configured, the assist is not enabled as we cannot compute embeddings. if a.embedder == nil { - // If the embedder is not configured, the assist is not enabled as we cannot compute embeddings. return &assist.IsAssistEnabledResponse{Enabled: false}, nil } + authCtx, err := a.authorizer.Authorize(ctx) + if err != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + + // Check if this endpoint is called by a user or Proxy. + if authz.IsLocalUser(*authCtx) { + checkErr := authCtx.Checker.CheckAccessToRule( + &services.Context{User: authCtx.User}, + defaults.Namespace, types.KindAssistant, types.VerbRead, + false, /* silent */ + ) + if checkErr != nil { + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) + } + } else { + // This endpoint is called from Proxy to check if the assist is enabled. + // Proxy credentials are used instead of the user credentials. + requestedByProxy := authz.HasBuiltinRole(*authCtx, string(types.RoleProxy)) + if !requestedByProxy { + return nil, trace.AccessDenied("only proxy is allowed to call IsAssistEnabled endpoint") + } + } + // Check if assist can use the backend. return a.backend.IsAssistEnabled(ctx) } @@ -144,7 +222,7 @@ func (a *Service) GetAssistantEmbeddings(ctx context.Context, msg *assist.GetAss // TODO(jakule): The kind needs to be updated when we add more resources. authCtx, err := authz.AuthorizeWithVerbs(ctx, a.log, a.authorizer, true, types.KindNode, types.VerbRead, types.VerbList) if err != nil { - return nil, trace.Wrap(err) + return nil, authz.ConvertAuthorizerError(ctx, a.log, err) } if a.embedder == nil { @@ -194,3 +272,8 @@ func (a *Service) GetAssistantEmbeddings(ctx context.Context, msg *assist.GetAss Embeddings: protoDocs, }, nil } + +// userHasAccess returns true if the user should have access to the resource. +func userHasAccess(authCtx *authz.Context, req interface{ GetUsername() string }) bool { + return !authz.IsCurrentUser(*authCtx, req.GetUsername()) && !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) +} diff --git a/lib/auth/assist/assistv1/service_test.go b/lib/auth/assist/assistv1/service_test.go new file mode 100644 index 0000000000000..ce06d8d38bbdf --- /dev/null +++ b/lib/auth/assist/assistv1/service_test.go @@ -0,0 +1,368 @@ +/* + * Copyright 2023 Gravitational, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package assistv1 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/timestamppb" + + assistpb "github.com/gravitational/teleport/api/gen/proto/go/assist/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/ai" + "github.com/gravitational/teleport/lib/assist" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/tlsca" +) + +const ( + defaultUser = "test-user" + noAccessUser = "user-no-access" +) + +func TestService_CreateAssistantConversation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + username string + req *assistpb.CreateAssistantConversationRequest + wantErr assert.ErrorAssertionFunc + assertResponse func(t *testing.T, resp *assistpb.CreateAssistantConversationResponse) + }{ + { + name: "success", + username: defaultUser, + req: &assistpb.CreateAssistantConversationRequest{ + Username: defaultUser, + CreatedTime: timestamppb.Now(), + }, + wantErr: assert.NoError, + assertResponse: func(t *testing.T, resp *assistpb.CreateAssistantConversationResponse) { + require.NotEmpty(t, resp.GetId()) + }, + }, + { + name: "access denies - RBAC", + username: noAccessUser, + req: &assistpb.CreateAssistantConversationRequest{ + Username: noAccessUser, + CreatedTime: timestamppb.Now(), + }, + wantErr: assert.Error, + }, + { + name: "access denied - different user", + username: defaultUser, + req: &assistpb.CreateAssistantConversationRequest{ + Username: noAccessUser, + CreatedTime: timestamppb.Now(), + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + got, err := svc.CreateAssistantConversation(ctxs[tt.username], tt.req) + tt.wantErr(t, err) + + if tt.assertResponse != nil { + tt.assertResponse(t, got) + } + }) + } +} + +func TestService_GetAssistantConversations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + username string + req *assistpb.GetAssistantConversationsRequest + wantErr assert.ErrorAssertionFunc + assertResponse func(t *testing.T, resp *assistpb.CreateAssistantConversationResponse) + }{ + { + name: "success", + username: defaultUser, + req: &assistpb.GetAssistantConversationsRequest{ + Username: defaultUser, + }, + wantErr: assert.NoError, + }, + { + name: "access denies - RBAC", + username: noAccessUser, + req: &assistpb.GetAssistantConversationsRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + { + name: "access denied - different user", + username: defaultUser, + req: &assistpb.GetAssistantConversationsRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + _, err := svc.GetAssistantConversations(ctxs[tt.username], tt.req) + tt.wantErr(t, err) + }) + } +} + +func TestService_DeleteAssistantConversations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + username string + req *assistpb.DeleteAssistantConversationRequest + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success", + username: defaultUser, + req: &assistpb.DeleteAssistantConversationRequest{ + Username: defaultUser, + }, + wantErr: assert.NoError, + }, + { + name: "access denies - RBAC", + username: noAccessUser, + req: &assistpb.DeleteAssistantConversationRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + { + name: "access denied - different user", + username: defaultUser, + req: &assistpb.DeleteAssistantConversationRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + // Create a conversation that we can remove, so we don't hit "conversation doesn't exist" error + convMsg, err := svc.backend.CreateAssistantConversation(ctxs[tt.username], &assistpb.CreateAssistantConversationRequest{ + Username: tt.username, + CreatedTime: timestamppb.Now(), + }) + require.NoError(t, err) + + conversationID := convMsg.GetId() + + tt.req.ConversationId = conversationID + + _, err = svc.DeleteAssistantConversation(ctxs[tt.username], tt.req) + tt.wantErr(t, err) + }) + } +} + +func TestService_InsertAssistantMessage(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + username string + req *assistpb.CreateAssistantMessageRequest + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success", + username: defaultUser, + req: &assistpb.CreateAssistantMessageRequest{ + Username: defaultUser, + Message: &assistpb.AssistantMessage{ + Type: string(assist.MessageKindAssistantMessage), + CreatedTime: timestamppb.Now(), + Payload: "Blah", + }, + }, + wantErr: assert.NoError, + }, + { + name: "access denies - RBAC", + username: noAccessUser, + req: &assistpb.CreateAssistantMessageRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + { + name: "access denied - different user", + username: defaultUser, + req: &assistpb.CreateAssistantMessageRequest{ + Username: noAccessUser, + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + // Create a conversation that we can remove, so we don't hit "conversation doesn't exist" error + convMsg, err := svc.backend.CreateAssistantConversation(ctxs[tt.username], &assistpb.CreateAssistantConversationRequest{ + Username: tt.username, + CreatedTime: timestamppb.Now(), + }) + require.NoError(t, err) + + conversationID := convMsg.GetId() + + tt.req.ConversationId = conversationID + + _, err = svc.CreateAssistantMessage(ctxs[tt.username], tt.req) + tt.wantErr(t, err) + }) + } +} + +func initSvc(t *testing.T) (map[string]context.Context, *Service) { + ctx := context.Background() + backend, err := memory.New(memory.Config{}) + require.NoError(t, err) + + clusterConfigSvc, err := local.NewClusterConfigurationService(backend) + require.NoError(t, err) + trustSvc := local.NewCAService(backend) + roleSvc := local.NewAccessService(backend) + userSvc := local.NewIdentityService(backend) + + require.NoError(t, clusterConfigSvc.SetAuthPreference(ctx, types.DefaultAuthPreference())) + require.NoError(t, clusterConfigSvc.SetClusterAuditConfig(ctx, types.DefaultClusterAuditConfig())) + require.NoError(t, clusterConfigSvc.SetClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig())) + require.NoError(t, clusterConfigSvc.SetSessionRecordingConfig(ctx, types.DefaultSessionRecordingConfig())) + + accessPoint := struct { + services.ClusterConfiguration + services.Trust + services.RoleGetter + services.UserGetter + }{ + ClusterConfiguration: clusterConfigSvc, + Trust: trustSvc, + RoleGetter: roleSvc, + UserGetter: userSvc, + } + + accessService := local.NewAccessService(backend) + eventService := local.NewEventsService(backend) + lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{ + ResourceWatcherConfig: services.ResourceWatcherConfig{ + Client: eventService, + Component: "test", + }, + LockGetter: accessService, + }) + require.NoError(t, err) + + authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + ClusterName: "test-cluster", + AccessPoint: accessPoint, + LockWatcher: lockWatcher, + }) + require.NoError(t, err) + + roles := map[string]types.Role{} + + role, err := types.NewRole("allow-rules", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + { + Resources: []string{types.KindAssistant}, + Verbs: []string{types.VerbList, types.VerbRead, types.VerbUpdate, types.VerbCreate, types.VerbDelete}, + }, + }, + }, + }) + require.NoError(t, err) + + roles[defaultUser] = role + + roleNoAccess, err := types.NewRole("no-rules", types.RoleSpecV6{ + Allow: types.RoleConditions{}, + }) + require.NoError(t, err) + roles["user-no-access"] = roleNoAccess + + ctxs := make(map[string]context.Context, len(roles)) + for username, role := range roles { + err = roleSvc.CreateRole(ctx, role) + require.NoError(t, err) + + user, err := types.NewUser(username) + user.AddRole(role.GetName()) + require.NoError(t, err) + + err = userSvc.CreateUser(user) + require.NoError(t, err) + + ctx = authz.ContextWithUser(ctx, authz.LocalUser{ + Username: user.GetName(), + Identity: tlsca.Identity{ + Username: user.GetName(), + Groups: []string{role.GetName()}, + }, + }) + ctxs[user.GetName()] = ctx + } + + svc, err := NewService(&ServiceConfig{ + Backend: local.NewAssistService(backend), + Authorizer: authorizer, + Embeddings: &ai.SimpleRetriever{}, + ResourceGetter: &nodeGetterFake{}, + }) + require.NoError(t, err) + + return ctxs, svc +} + +type nodeGetterFake struct { +} + +func (g *nodeGetterFake) GetNode(ctx context.Context, namespace, name string) (types.Server, error) { + return nil, nil +} diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 3280c682895f4..6d8d30f0b4872 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -59,7 +59,6 @@ import ( "github.com/gravitational/teleport/api/constants" apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/gen/proto/go/assist/v1" - userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" "github.com/gravitational/teleport/api/internalutils/stream" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" @@ -5370,17 +5369,6 @@ func (a *Server) CompareAndSwapHeadlessAuthentication(ctx context.Context, old, return headlessAuthn, trace.Wrap(err) } -// GetUserPreferences returns the user preferences for a given user. -func (a *Server) GetUserPreferences(ctx context.Context, request *userpreferencesv1.GetUserPreferencesRequest) (*userpreferencesv1.GetUserPreferencesResponse, error) { - resp, err := a.Services.GetUserPreferences(ctx, request) - return resp, trace.Wrap(err) -} - -// UpsertUserPreferences creates or updates user preferences for a given username. -func (a *Server) UpsertUserPreferences(ctx context.Context, request *userpreferencesv1.UpsertUserPreferencesRequest) error { - return trace.Wrap(a.Services.UpsertUserPreferences(ctx, request)) -} - // getProxyPublicAddr returns the first valid, non-empty proxy public address it // finds, or empty otherwise. func (a *Server) getProxyPublicAddr() string { diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 5ee74890070d6..a5baad0d49020 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -236,7 +236,7 @@ func (a *ServerWithRoles) isLocalOrRemoteServerAction() bool { // whether any of the given roles match the role set. func (a *ServerWithRoles) hasBuiltinRole(roles ...types.SystemRole) bool { for _, role := range roles { - if HasBuiltinRole(a.context, string(role)) { + if authz.HasBuiltinRole(a.context, string(role)) { return true } } @@ -245,15 +245,11 @@ func (a *ServerWithRoles) hasBuiltinRole(roles ...types.SystemRole) bool { // HasBuiltinRole checks if the identity is a builtin role with the matching // name. +// Deprecated: use authz.HasBuiltinRole instead. func HasBuiltinRole(authContext authz.Context, name string) bool { - if _, ok := authContext.Identity.(authz.BuiltinRole); !ok { - return false - } - if !authContext.Checker.HasRole(name) { - return false - } - - return true + // TODO(jakule): This function can be removed once teleport.e is updated + // to use authz.HasBuiltinRole. + return authz.HasBuiltinRole(authContext, name) } // HasRemoteBuiltinRole checks if the identity is a remote builtin role with the @@ -5508,8 +5504,8 @@ func (a *ServerWithRoles) checkAccessToNode(node types.Server) error { // In addition, allow proxy (and remote proxy) to access all nodes for its // smart resolution address resolution. Once the smart resolution logic is // moved to the auth server, this logic can be removed. - builtinRole := HasBuiltinRole(a.context, string(types.RoleAdmin)) || - HasBuiltinRole(a.context, string(types.RoleProxy)) || + builtinRole := authz.HasBuiltinRole(a.context, string(types.RoleAdmin)) || + authz.HasBuiltinRole(a.context, string(types.RoleProxy)) || HasRemoteBuiltinRole(a.context, string(types.RoleRemoteProxy)) if builtinRole { @@ -6438,88 +6434,47 @@ func (a *ServerWithRoles) WatchPendingHeadlessAuthentications(ctx context.Contex // CreateAssistantConversation creates a new conversation entry in the backend. func (a *ServerWithRoles) CreateAssistantConversation(ctx context.Context, req *assist.CreateAssistantConversationRequest) (*assist.CreateAssistantConversationResponse, error) { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbCreate); err != nil { - return nil, trace.Wrap(err) - } - - return a.authServer.CreateAssistantConversation(ctx, req) + return nil, trace.NotImplemented("CreateAssistantConversation must not be called on auth.ServerWithRoles") } // GetAssistantConversations returns all conversations started by a user. func (a *ServerWithRoles) GetAssistantConversations(ctx context.Context, request *assist.GetAssistantConversationsRequest) (*assist.GetAssistantConversationsResponse, error) { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbList); err != nil { - return nil, trace.Wrap(err) - } - - return a.authServer.GetAssistantConversations(ctx, request) + return nil, trace.NotImplemented("GetAssistantConversations must not be called on auth.ServerWithRoles") } // GetAssistantMessages returns all messages with given conversation ID. func (a *ServerWithRoles) GetAssistantMessages(ctx context.Context, req *assist.GetAssistantMessagesRequest) (*assist.GetAssistantMessagesResponse, error) { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbRead); err != nil { - return nil, trace.Wrap(err) - } - - return a.authServer.GetAssistantMessages(ctx, req) + return nil, trace.NotImplemented("GetAssistantMessages must not be called on auth.ServerWithRoles") } // DeleteAssistantConversation deletes a conversation by ID. func (a *ServerWithRoles) DeleteAssistantConversation(ctx context.Context, req *assist.DeleteAssistantConversationRequest) error { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbDelete); err != nil { - return trace.Wrap(err) - } - - return trace.Wrap(a.authServer.DeleteAssistantConversation(ctx, req)) + return trace.NotImplemented("DeleteAssistantConversation must not be called on auth.ServerWithRoles") } // IsAssistEnabled returns true if the assist is enabled or not on the auth level. func (a *ServerWithRoles) IsAssistEnabled(ctx context.Context) (*assist.IsAssistEnabledResponse, error) { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbRead); err != nil { - return nil, trace.Wrap(err) - } - - return a.authServer.IsAssistEnabled(ctx) + return nil, trace.NotImplemented("IsAssistEnabled must not be called on auth.ServerWithRoles") } // CreateAssistantMessage adds the message to the backend. func (a *ServerWithRoles) CreateAssistantMessage(ctx context.Context, msg *assist.CreateAssistantMessageRequest) error { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbUpdate); err != nil { - return trace.Wrap(err) - } - - return a.authServer.CreateAssistantMessage(ctx, msg) + return trace.NotImplemented("CreateAssistantMessage must not be called on auth.ServerWithRoles") } // UpdateAssistantConversationInfo updates the conversation info. func (a *ServerWithRoles) UpdateAssistantConversationInfo(ctx context.Context, msg *assist.UpdateAssistantConversationInfoRequest) error { - if err := a.action(apidefaults.Namespace, types.KindAssistant, types.VerbUpdate); err != nil { - return trace.Wrap(err) - } - - return a.authServer.UpdateAssistantConversationInfo(ctx, msg) + return trace.NotImplemented("UpdateAssistantConversationInfo must not be called on auth.ServerWithRoles") } // GetUserPreferences returns the user preferences for a given user. func (a *ServerWithRoles) GetUserPreferences(ctx context.Context, req *userpreferencespb.GetUserPreferencesRequest) (*userpreferencespb.GetUserPreferencesResponse, error) { - if err := a.currentUserAction(req.Username); err != nil { - return nil, trace.Wrap(err) - } - - preferences, err := a.authServer.GetUserPreferences(ctx, req) - if err != nil { - return nil, trace.Wrap(err) - } - - return preferences, nil + return nil, trace.NotImplemented("GetUserPreferences must not be called on auth.ServerWithRoles") } // UpsertUserPreferences creates or updates user preferences for a given username. func (a *ServerWithRoles) UpsertUserPreferences(ctx context.Context, req *userpreferencespb.UpsertUserPreferencesRequest) error { - if err := a.currentUserAction(req.Username); err != nil { - return trace.Wrap(err) - } - - return trace.Wrap(a.authServer.UpsertUserPreferences(ctx, req)) + return trace.NotImplemented("UpsertUserPreferences must not be called on auth.ServerWithRoles") } // CloneHTTPClient creates a new HTTP client with the same configuration. diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 62456a0b90f65..b6ae7f1662224 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -33,6 +33,7 @@ import ( loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1" pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" samlidppb "github.com/gravitational/teleport/api/gen/proto/go/teleport/samlidp/v1" + userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" "github.com/gravitational/teleport/api/internalutils/stream" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" @@ -691,7 +692,6 @@ type ClientI interface { services.SAMLIdPServiceProviders services.UserGroups services.Assistant - services.UserPreferences WebService services.Status services.ClusterConfiguration @@ -848,4 +848,10 @@ type ClientI interface { // GetResources returns a paginated list of resources. GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) + + // GetUserPreferences returns the user preferences for a given user. + GetUserPreferences(ctx context.Context, req *userpreferencesv1.GetUserPreferencesRequest) (*userpreferencesv1.GetUserPreferencesResponse, error) + + // UpsertUserPreferences creates or updates user preferences for a given username. + UpsertUserPreferences(ctx context.Context, req *userpreferencesv1.UpsertUserPreferencesRequest) error } diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 578e9033b8005..2e9d61d11f30b 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -5458,7 +5458,8 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { // Initialize and register the user preferences service. userPreferencesSrv, err := userpreferencesv1.NewService(&userpreferencesv1.ServiceConfig{ - Backend: cfg.AuthServer.Services, + Backend: cfg.AuthServer.Services, + Authorizer: cfg.Authorizer, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/userpreferences/userpreferencesv1/service.go b/lib/auth/userpreferences/userpreferencesv1/service.go index 933c3f9fe8d54..37393f0c770ad 100644 --- a/lib/auth/userpreferences/userpreferencesv1/service.go +++ b/lib/auth/userpreferences/userpreferencesv1/service.go @@ -21,22 +21,28 @@ import ( "context" "github.com/gravitational/trace" + "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/emptypb" userpreferences "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" + "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/services" ) // ServiceConfig holds configuration options for the user preferences service. type ServiceConfig struct { - Backend services.UserPreferences + Backend services.UserPreferences + Authorizer authz.Authorizer + Logger *logrus.Entry } // Service implements the teleport.userpreferences.v1.UserPreferencesService RPC service. type Service struct { userpreferences.UnimplementedUserPreferencesServiceServer - backend services.UserPreferences + backend services.UserPreferences + authorizer authz.Authorizer + log *logrus.Entry } // NewService returns a new user preferences gRPC service. @@ -44,19 +50,53 @@ func NewService(cfg *ServiceConfig) (*Service, error) { switch { case cfg.Backend == nil: return nil, trace.BadParameter("backend is required") + case cfg.Authorizer == nil: + return nil, trace.BadParameter("authorizer is required") + case cfg.Logger == nil: + cfg.Logger = logrus.WithField(trace.Component, "userpreferences.service") } return &Service{ - backend: cfg.Backend, + backend: cfg.Backend, + authorizer: cfg.Authorizer, + log: cfg.Logger, }, nil } // GetUserPreferences returns the user preferences for a given user. -func (a *Service) GetUserPreferences(ctx context.Context, req *userpreferences.GetUserPreferencesRequest) (*userpreferences.GetUserPreferencesResponse, error) { - return a.backend.GetUserPreferences(ctx, req) +func (a *Service) GetUserPreferences(ctx context.Context, _ *userpreferences.GetUserPreferencesRequest) (*userpreferences.GetUserPreferencesResponse, error) { + authCtx, err := a.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if !authz.IsLocalUser(*authCtx) { + return nil, trace.AccessDenied("Non-local user cannot get user preferences") + } + + username := authCtx.User.GetName() + + prefs, err := a.backend.GetUserPreferences(ctx, username) + if err != nil { + return nil, trace.Wrap(err) + } + + return &userpreferences.GetUserPreferencesResponse{ + Preferences: prefs, + }, nil } // UpsertUserPreferences creates or updates user preferences for a given username. func (a *Service) UpsertUserPreferences(ctx context.Context, req *userpreferences.UpsertUserPreferencesRequest) (*emptypb.Empty, error) { - return &emptypb.Empty{}, trace.Wrap(a.backend.UpsertUserPreferences(ctx, req)) + authCtx, err := a.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + if !authz.IsLocalUser(*authCtx) { + return nil, trace.AccessDenied("Non-local user cannot upsert user preferences") + } + + username := authCtx.User.GetName() + + return &emptypb.Empty{}, trace.Wrap(a.backend.UpsertUserPreferences(ctx, username, req.Preferences)) } diff --git a/lib/auth/userpreferences/userpreferencesv1/service_test.go b/lib/auth/userpreferences/userpreferencesv1/service_test.go new file mode 100644 index 0000000000000..6b15f8980fd89 --- /dev/null +++ b/lib/auth/userpreferences/userpreferencesv1/service_test.go @@ -0,0 +1,223 @@ +/* + * Copyright 2023 Gravitational, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package userpreferencesv1 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/backend/memory" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/services/local" + "github.com/gravitational/teleport/lib/tlsca" +) + +const ( + defaultUser = "test-user" + nonExistingUser = "non-existing-user" +) + +func TestService_GetUserPreferences(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + userName string + req *userpreferencesv1.GetUserPreferencesRequest + want *userpreferencesv1.GetUserPreferencesResponse + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success", + userName: defaultUser, + req: &userpreferencesv1.GetUserPreferencesRequest{}, + want: &userpreferencesv1.GetUserPreferencesResponse{ + Preferences: &userpreferencesv1.UserPreferences{ + Assist: &userpreferencesv1.AssistUserPreferences{ + PreferredLogins: []string{}, + ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED, + }, + Theme: userpreferencesv1.Theme_THEME_LIGHT, + Onboard: &userpreferencesv1.OnboardUserPreferences{ + PreferredResources: []userpreferencesv1.Resource{}, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "access denied - user doesn't exist", + userName: nonExistingUser, + req: &userpreferencesv1.GetUserPreferencesRequest{}, + want: nil, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + got, err := svc.GetUserPreferences(ctxs[tt.userName], tt.req) + tt.wantErr(t, err) + + if tt.want != nil { + require.Equal(t, tt.want, got) + } + }) + } +} + +func TestService_UpsertUserPreferences(t *testing.T) { + t.Parallel() + + defaultPreferences := &userpreferencesv1.UserPreferences{ + Assist: &userpreferencesv1.AssistUserPreferences{ + PreferredLogins: []string{}, + ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED, + }, + Theme: userpreferencesv1.Theme_THEME_LIGHT, + Onboard: &userpreferencesv1.OnboardUserPreferences{ + PreferredResources: []userpreferencesv1.Resource{}, + }, + } + + tests := []struct { + name string + userName string + req *userpreferencesv1.UpsertUserPreferencesRequest + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success", + userName: defaultUser, + req: &userpreferencesv1.UpsertUserPreferencesRequest{ + Preferences: defaultPreferences, + }, + wantErr: assert.NoError, + }, + { + name: "access denied - user doesn't exist", + userName: nonExistingUser, + req: &userpreferencesv1.UpsertUserPreferencesRequest{ + Preferences: defaultPreferences, + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxs, svc := initSvc(t) + + _, err := svc.UpsertUserPreferences(ctxs[tt.userName], tt.req) + if tt.wantErr(t, err) { + return + } + }) + } +} + +func initSvc(t *testing.T) (map[string]context.Context, *Service) { + ctx := context.Background() + backend, err := memory.New(memory.Config{}) + require.NoError(t, err) + + clusterConfigSvc, err := local.NewClusterConfigurationService(backend) + require.NoError(t, err) + trustSvc := local.NewCAService(backend) + roleSvc := local.NewAccessService(backend) + userSvc := local.NewIdentityService(backend) + + require.NoError(t, clusterConfigSvc.SetAuthPreference(ctx, types.DefaultAuthPreference())) + require.NoError(t, clusterConfigSvc.SetClusterAuditConfig(ctx, types.DefaultClusterAuditConfig())) + require.NoError(t, clusterConfigSvc.SetClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig())) + require.NoError(t, clusterConfigSvc.SetSessionRecordingConfig(ctx, types.DefaultSessionRecordingConfig())) + + accessPoint := struct { + services.ClusterConfiguration + services.Trust + services.RoleGetter + services.UserGetter + }{ + ClusterConfiguration: clusterConfigSvc, + Trust: trustSvc, + RoleGetter: roleSvc, + UserGetter: userSvc, + } + + accessService := local.NewAccessService(backend) + eventService := local.NewEventsService(backend) + lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{ + ResourceWatcherConfig: services.ResourceWatcherConfig{ + Client: eventService, + Component: "test", + }, + LockGetter: accessService, + }) + require.NoError(t, err) + + authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ + ClusterName: "test-cluster", + AccessPoint: accessPoint, + LockWatcher: lockWatcher, + }) + require.NoError(t, err) + + roles := map[string]types.Role{} + + role, err := types.NewRole("allow-rules", types.RoleSpecV6{}) + require.NoError(t, err) + + roles[defaultUser] = role + + ctxs := make(map[string]context.Context, len(roles)) + for username, role := range roles { + err = roleSvc.CreateRole(ctx, role) + require.NoError(t, err) + + user, err := types.NewUser(username) + user.AddRole(role.GetName()) + require.NoError(t, err) + + err = userSvc.CreateUser(user) + require.NoError(t, err) + + ctx = authz.ContextWithUser(ctx, authz.LocalUser{ + Username: user.GetName(), + Identity: tlsca.Identity{ + Username: user.GetName(), + Groups: []string{role.GetName()}, + }, + }) + ctxs[user.GetName()] = ctx + } + + svc, err := NewService(&ServiceConfig{ + Backend: local.NewUserPreferencesService(backend), + Authorizer: authorizer, + }) + require.NoError(t, err) + + return ctxs, svc +} diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index 2c398dafd733c..2943ef65b9370 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -1355,3 +1355,27 @@ func UserFromContext(ctx context.Context) (IdentityGetter, error) { } return user, nil } + +// HasBuiltinRole checks if the identity is a builtin role with the matching +// name. +func HasBuiltinRole(authContext Context, name string) bool { + if _, ok := authContext.Identity.(BuiltinRole); !ok { + return false + } + if !authContext.Checker.HasRole(name) { + return false + } + + return true +} + +// IsLocalUser checks if the identity is a local user. +func IsLocalUser(authContext Context) bool { + _, ok := authContext.Identity.(LocalUser) + return ok +} + +// IsCurrentUser checks if the identity is a local user matching the given username +func IsCurrentUser(authContext Context, username string) bool { + return IsLocalUser(authContext) && authContext.User.GetName() == username +} diff --git a/lib/services/local/userpreferences.go b/lib/services/local/userpreferences.go index f237c35787790..e36a9dd61a415 100644 --- a/lib/services/local/userpreferences.go +++ b/lib/services/local/userpreferences.go @@ -55,29 +55,29 @@ func NewUserPreferencesService(backend backend.Backend) *UserPreferencesService } // GetUserPreferences returns the user preferences for the given user. -func (u *UserPreferencesService) GetUserPreferences(ctx context.Context, req *userpreferencesv1.GetUserPreferencesRequest) (*userpreferencesv1.GetUserPreferencesResponse, error) { - preferences, err := u.getUserPreferences(ctx, req.Username) +func (u *UserPreferencesService) GetUserPreferences(ctx context.Context, username string) (*userpreferencesv1.UserPreferences, error) { + preferences, err := u.getUserPreferences(ctx, username) if err != nil { if trace.IsNotFound(err) { - return &userpreferencesv1.GetUserPreferencesResponse{Preferences: DefaultUserPreferences()}, nil + return DefaultUserPreferences(), nil } return nil, trace.Wrap(err) } - return &userpreferencesv1.GetUserPreferencesResponse{Preferences: preferences}, nil + return preferences, nil } // UpsertUserPreferences creates or updates user preferences for a given username. -func (u *UserPreferencesService) UpsertUserPreferences(ctx context.Context, req *userpreferencesv1.UpsertUserPreferencesRequest) error { - if req.Username == "" { +func (u *UserPreferencesService) UpsertUserPreferences(ctx context.Context, username string, prefs *userpreferencesv1.UserPreferences) error { + if username == "" { return trace.BadParameter("missing username") } - if err := validatePreferences(req.Preferences); err != nil { + if err := validatePreferences(prefs); err != nil { return trace.Wrap(err) } - preferences, err := u.getUserPreferences(ctx, req.Username) + preferences, err := u.getUserPreferences(ctx, username) if err != nil { if !trace.IsNotFound(err) { return trace.Wrap(err) @@ -85,11 +85,11 @@ func (u *UserPreferencesService) UpsertUserPreferences(ctx context.Context, req preferences = DefaultUserPreferences() } - if err := overwriteValues(preferences, req.Preferences); err != nil { + if err := overwriteValues(preferences, prefs); err != nil { return trace.Wrap(err) } - item, err := createBackendItem(req.Username, preferences) + item, err := createBackendItem(username, preferences) if err != nil { return trace.Wrap(err) } diff --git a/lib/services/local/userpreferences_test.go b/lib/services/local/userpreferences_test.go index 0bcf015c773a4..dcfeace7ee75a 100644 --- a/lib/services/local/userpreferences_test.go +++ b/lib/services/local/userpreferences_test.go @@ -63,7 +63,6 @@ func TestUserPreferencesCRUD2(t *testing.T) { { name: "update the theme preference only", req: &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: username, Preferences: &userpreferencesv1.UserPreferences{ Theme: userpreferencesv1.Theme_THEME_DARK, }, @@ -77,7 +76,6 @@ func TestUserPreferencesCRUD2(t *testing.T) { { name: "update the assist preferred logins only", req: &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: username, Preferences: &userpreferencesv1.UserPreferences{ Assist: &userpreferencesv1.AssistUserPreferences{ PreferredLogins: []string{"foo", "bar"}, @@ -99,7 +97,6 @@ func TestUserPreferencesCRUD2(t *testing.T) { { name: "update the assist view mode only", req: &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: username, Preferences: &userpreferencesv1.UserPreferences{ Assist: &userpreferencesv1.AssistUserPreferences{ ViewMode: userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_POPUP_EXPANDED_SIDEBAR_VISIBLE, @@ -118,7 +115,6 @@ func TestUserPreferencesCRUD2(t *testing.T) { { name: "update the onboard preference only", req: &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: username, Preferences: &userpreferencesv1.UserPreferences{ Onboard: &userpreferencesv1.OnboardUserPreferences{ PreferredResources: []userpreferencesv1.Resource{userpreferencesv1.Resource_RESOURCE_DATABASES}, @@ -136,7 +132,6 @@ func TestUserPreferencesCRUD2(t *testing.T) { { name: "update all the settings at once", req: &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: username, Preferences: &userpreferencesv1.UserPreferences{ Theme: userpreferencesv1.Theme_THEME_LIGHT, Assist: &userpreferencesv1.AssistUserPreferences{ @@ -168,24 +163,20 @@ func TestUserPreferencesCRUD2(t *testing.T) { identity := newUserPreferencesService(t) - res, err := identity.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{ - Username: username, - }) + res, err := identity.GetUserPreferences(ctx, username) require.NoError(t, err) // Clone the proto as the accessing fields for some reason modifies the state. - require.Empty(t, cmp.Diff(defaultPref, proto.Clone(res.Preferences), protocmp.Transform())) + require.Empty(t, cmp.Diff(defaultPref, proto.Clone(res), protocmp.Transform())) if test.req != nil { - err := identity.UpsertUserPreferences(ctx, test.req) + err := identity.UpsertUserPreferences(ctx, username, test.req.Preferences) require.NoError(t, err) } - res, err = identity.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{ - Username: username, - }) + res, err = identity.GetUserPreferences(ctx, username) require.NoError(t, err) - require.Empty(t, cmp.Diff(test.expected, res.Preferences, protocmp.Transform())) + require.Empty(t, cmp.Diff(test.expected, res, protocmp.Transform())) }) } } @@ -213,15 +204,13 @@ func TestLayoutUpdate(t *testing.T) { require.NoError(t, err) // Get the preferences and ensure that the layout is updated. - prefs, err := identity.GetUserPreferences(ctx, &userpreferencesv1.GetUserPreferencesRequest{ - Username: "test", - }) + prefs, err := identity.GetUserPreferences(ctx, "test") require.NoError(t, err) // The layout should be updated to the latest version (values should not be nil). - require.NotNil(t, prefs.Preferences.Onboard) + require.NotNil(t, prefs.Onboard) // Non-existing values should be set to the default value. - require.Equal(t, userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED, prefs.Preferences.Assist.ViewMode) - require.Equal(t, userpreferencesv1.Theme_THEME_LIGHT, prefs.Preferences.Theme) + require.Equal(t, userpreferencesv1.AssistViewMode_ASSIST_VIEW_MODE_DOCKED, prefs.Assist.ViewMode) + require.Equal(t, userpreferencesv1.Theme_THEME_LIGHT, prefs.Theme) // Existing values should be preserved. - require.Equal(t, []string{"foo", "bar"}, prefs.Preferences.Assist.PreferredLogins) + require.Equal(t, []string{"foo", "bar"}, prefs.Assist.PreferredLogins) } diff --git a/lib/services/userpreferences.go b/lib/services/userpreferences.go index fcc683e7f3128..3f2539be32bda 100644 --- a/lib/services/userpreferences.go +++ b/lib/services/userpreferences.go @@ -27,7 +27,7 @@ import ( // UserPreferences is the interface for managing user preferences. type UserPreferences interface { // GetUserPreferences returns the user preferences for a given user. - GetUserPreferences(ctx context.Context, req *userpreferencesv1.GetUserPreferencesRequest) (*userpreferencesv1.GetUserPreferencesResponse, error) + GetUserPreferences(ctx context.Context, username string) (*userpreferencesv1.UserPreferences, error) // UpsertUserPreferences creates or updates user preferences for a given username. - UpsertUserPreferences(ctx context.Context, req *userpreferencesv1.UpsertUserPreferencesRequest) error + UpsertUserPreferences(ctx context.Context, username string, prefs *userpreferencesv1.UserPreferences) error } diff --git a/lib/web/assistant_test.go b/lib/web/assistant_test.go index cce3bc1fa3f0e..55c7dd1b96122 100644 --- a/lib/web/assistant_test.go +++ b/lib/web/assistant_test.go @@ -37,9 +37,11 @@ import ( "golang.org/x/time/rate" authproto "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/types" aitest "github.com/gravitational/teleport/lib/ai/testutils" "github.com/gravitational/teleport/lib/assist" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/services" ) func Test_runAssistant(t *testing.T) { @@ -150,8 +152,18 @@ func Test_runAssistant(t *testing.T) { tc.setup(t, s) } + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + ctx := context.Background() - authPack := s.authPack(t, "foo") + authPack := s.authPack(t, "foo", assistRole.GetName()) // Create the conversation conversationID := s.makeAssistConversation(t, ctx, authPack) @@ -239,7 +251,18 @@ func Test_runAssistError(t *testing.T) { s := newWebSuiteWithConfig(t, webSuiteConfig{OpenAIConfig: &openaiCfg}) ctx := context.Background() - authPack := s.authPack(t, "foo") + + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + + authPack := s.authPack(t, "foo", assistRole.GetName()) // Create the conversation conversationID := s.makeAssistConversation(t, ctx, authPack) @@ -337,7 +360,17 @@ func Test_generateAssistantTitle(t *testing.T) { OpenAIConfig: &openaiCfg, }) - pack := s.authPack(t, "foo") + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + + pack := s.authPack(t, "foo", assistRole.GetName()) // Real test: we craft a request asking for a summary endpoint := pack.clt.Endpoint("webapi", "assistant", "title", "summary") diff --git a/lib/web/command_test.go b/lib/web/command_test.go index c2eca4a0eeeda..2b3c7bc9ca24f 100644 --- a/lib/web/command_test.go +++ b/lib/web/command_test.go @@ -42,10 +42,12 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport/api/gen/proto/go/assist/v1" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/ai/testutils" assistlib "github.com/gravitational/teleport/lib/assist" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/utils" ) @@ -65,7 +67,17 @@ func TestExecuteCommand(t *testing.T) { OpenAIConfig: &openAIConfig, }) - ws, _, err := s.makeCommand(t, s.authPack(t, testUser), uuid.New()) + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + + ws, _, err := s.makeCommand(t, s.authPack(t, testUser, assistRole.GetName()), uuid.New()) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, ws.Close()) }) @@ -84,7 +96,18 @@ func TestExecuteCommandHistory(t *testing.T) { disableDiskBasedRecording: true, OpenAIConfig: &openAIConfig, }) - authPack := s.authPack(t, testUser) + + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + + authPack := s.authPack(t, testUser, assistRole.GetName()) ctx := context.Background() clt, err := s.server.NewClient(auth.TestUser(testUser)) @@ -115,7 +138,7 @@ func TestExecuteCommandHistory(t *testing.T) { // Then command execution history is saved var messages *assist.GetAssistantMessagesResponse - // Command execution history is saved in asynchronusly, so we need to wait for it. + // Command execution history is saved in asynchronously, so we need to wait for it. require.Eventually(t, func() bool { messages, err = clt.GetAssistantMessages(ctx, &assist.GetAssistantMessagesRequest{ ConversationId: conversationID.String(), @@ -152,7 +175,18 @@ func TestExecuteCommandSummary(t *testing.T) { disableDiskBasedRecording: true, OpenAIConfig: &openAIConfig, }) - authPack := s.authPack(t, testUser) + + assistRole, err := types.NewRole("assist-access", types.RoleSpecV6{ + Allow: types.RoleConditions{ + Rules: []types.Rule{ + types.NewRule(types.KindAssistant, services.RW()), + }, + }, + }) + require.NoError(t, err) + require.NoError(t, s.server.Auth().UpsertRole(s.ctx, assistRole)) + + authPack := s.authPack(t, testUser, assistRole.GetName()) ctx := context.Background() clt, err := s.server.NewClient(auth.TestUser(testUser)) diff --git a/lib/web/userpreferences.go b/lib/web/userpreferences.go index f5ae62ffe879f..0c247d4862e0a 100644 --- a/lib/web/userpreferences.go +++ b/lib/web/userpreferences.go @@ -50,9 +50,7 @@ func (h *Handler) getUserPreferences(_ http.ResponseWriter, r *http.Request, p h return nil, trace.Wrap(err) } - resp, err := authClient.GetUserPreferences(r.Context(), &userpreferencesv1.GetUserPreferencesRequest{ - Username: sctx.GetUser(), - }) + resp, err := authClient.GetUserPreferences(r.Context(), &userpreferencesv1.GetUserPreferencesRequest{}) if err != nil { return nil, trace.Wrap(err) } @@ -74,7 +72,6 @@ func (h *Handler) updateUserPreferences(_ http.ResponseWriter, r *http.Request, } preferences := &userpreferencesv1.UpsertUserPreferencesRequest{ - Username: sctx.GetUser(), Preferences: &userpreferencesv1.UserPreferences{ Theme: req.Theme, Assist: &userpreferencesv1.AssistUserPreferences{