From 6962f77dd36cbc965947ec6b89965326f6406e18 Mon Sep 17 00:00:00 2001 From: Yassine Bounekhla <56373201+rudream@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:36:09 -0400 Subject: [PATCH] Add tctl notifications commands (#42124) (#45503) --- api/client/client.go | 24 + .../v1/notifications_service.pb.go | 447 +++++++++++------- .../v1/notifications_service.proto | 12 + api/types/constants.go | 2 + lib/auth/authclient/clt.go | 34 +- .../notifications/notificationsv1/service.go | 200 ++++++++ lib/services/presets.go | 2 + lib/web/ui/notification.go | 26 +- tool/tctl/common/cmds.go | 1 + tool/tctl/common/helpers_test.go | 10 + tool/tctl/common/notification_command.go | 313 ++++++++++++ tool/tctl/common/notification_command_test.go | 113 +++++ .../src/Notifications/Notification.tsx | 22 + .../notificationContentFactory.tsx | 7 +- .../services/notifications/notifications.ts | 4 +- .../src/services/notifications/types.ts | 4 + 16 files changed, 1019 insertions(+), 202 deletions(-) create mode 100644 tool/tctl/common/notification_command.go create mode 100644 tool/tctl/common/notification_command_test.go diff --git a/api/client/client.go b/api/client/client.go index 22f8e31005413..61aa64698b2fd 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -4863,6 +4863,30 @@ func (c *Client) ListNotifications(ctx context.Context, req *notificationsv1pb.L return rsp, trace.Wrap(err) } +// CreateGlobalNotification creates a global notification. +func (c *Client) CreateGlobalNotification(ctx context.Context, req *notificationsv1pb.CreateGlobalNotificationRequest) (*notificationsv1pb.GlobalNotification, error) { + rsp, err := c.NotificationServiceClient().CreateGlobalNotification(ctx, req) + return rsp, trace.Wrap(err) +} + +// CreateUserNotification creates a user-specific notification. +func (c *Client) CreateUserNotification(ctx context.Context, req *notificationsv1pb.CreateUserNotificationRequest) (*notificationsv1pb.Notification, error) { + rsp, err := c.NotificationServiceClient().CreateUserNotification(ctx, req) + return rsp, trace.Wrap(err) +} + +// DeleteGlobalNotification deletes a global notification. +func (c *Client) DeleteGlobalNotification(ctx context.Context, req *notificationsv1pb.DeleteGlobalNotificationRequest) error { + _, err := c.NotificationServiceClient().DeleteGlobalNotification(ctx, req) + return trace.Wrap(err) +} + +// DeleteUserNotification not implemented: can only be called locally. +func (c *Client) DeleteUserNotification(ctx context.Context, req *notificationsv1pb.DeleteUserNotificationRequest) error { + _, err := c.NotificationServiceClient().DeleteUserNotification(ctx, req) + return trace.Wrap(err) +} + // UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification. func (c *Client) UpsertUserNotificationState(ctx context.Context, req *notificationsv1pb.UpsertUserNotificationStateRequest) (*notificationsv1pb.UserNotificationState, error) { rsp, err := c.NotificationServiceClient().UpsertUserNotificationState(ctx, req) diff --git a/api/gen/proto/go/teleport/notifications/v1/notifications_service.pb.go b/api/gen/proto/go/teleport/notifications/v1/notifications_service.pb.go index af5b92c2b3d7c..df5715ffb5fe4 100644 --- a/api/gen/proto/go/teleport/notifications/v1/notifications_service.pb.go +++ b/api/gen/proto/go/teleport/notifications/v1/notifications_service.pb.go @@ -165,6 +165,8 @@ type ListNotificationsRequest struct { PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` // page_token is the next_page_token value returned from a previous ListUserNotifications request, if any. PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // filters specify search criteria to limit which notifications should be returned. If omitted, the default behavior will be to list all notifications. + Filters *NotificationFilters `protobuf:"bytes,3,opt,name=filters,proto3" json:"filters,omitempty"` } func (x *ListNotificationsRequest) Reset() { @@ -213,6 +215,80 @@ func (x *ListNotificationsRequest) GetPageToken() string { return "" } +func (x *ListNotificationsRequest) GetFilters() *NotificationFilters { + if x != nil { + return x.Filters + } + return nil +} + +// NotificationFilters provide a mechanism to refine ListNotification results. +type NotificationFilters struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // username is the username of the user the notifications being listed are for. + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + // global_only is whether to only list global notifications (notifications capable of targetting multiple users). + GlobalOnly bool `protobuf:"varint,2,opt,name=global_only,json=globalOnly,proto3" json:"global_only,omitempty"` + // user_created_only is whether to only list user-created notifications (ie. notifications created by an admin via the tctl interface). + UserCreatedOnly bool `protobuf:"varint,3,opt,name=user_created_only,json=userCreatedOnly,proto3" json:"user_created_only,omitempty"` +} + +func (x *NotificationFilters) Reset() { + *x = NotificationFilters{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NotificationFilters) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NotificationFilters) ProtoMessage() {} + +func (x *NotificationFilters) ProtoReflect() protoreflect.Message { + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NotificationFilters.ProtoReflect.Descriptor instead. +func (*NotificationFilters) Descriptor() ([]byte, []int) { + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{3} +} + +func (x *NotificationFilters) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *NotificationFilters) GetGlobalOnly() bool { + if x != nil { + return x.GlobalOnly + } + return false +} + +func (x *NotificationFilters) GetUserCreatedOnly() bool { + if x != nil { + return x.UserCreatedOnly + } + return false +} + // ListNotificationsResponse is the response from listing a user's notifications. type ListNotificationsResponse struct { state protoimpl.MessageState @@ -230,7 +306,7 @@ type ListNotificationsResponse struct { func (x *ListNotificationsResponse) Reset() { *x = ListNotificationsResponse{} if protoimpl.UnsafeEnabled { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[3] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -243,7 +319,7 @@ func (x *ListNotificationsResponse) String() string { func (*ListNotificationsResponse) ProtoMessage() {} func (x *ListNotificationsResponse) ProtoReflect() protoreflect.Message { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[3] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -256,7 +332,7 @@ func (x *ListNotificationsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListNotificationsResponse.ProtoReflect.Descriptor instead. func (*ListNotificationsResponse) Descriptor() ([]byte, []int) { - return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{3} + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{4} } func (x *ListNotificationsResponse) GetNotifications() []*Notification { @@ -293,7 +369,7 @@ type CreateGlobalNotificationRequest struct { func (x *CreateGlobalNotificationRequest) Reset() { *x = CreateGlobalNotificationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[4] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -306,7 +382,7 @@ func (x *CreateGlobalNotificationRequest) String() string { func (*CreateGlobalNotificationRequest) ProtoMessage() {} func (x *CreateGlobalNotificationRequest) ProtoReflect() protoreflect.Message { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[4] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -319,7 +395,7 @@ func (x *CreateGlobalNotificationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateGlobalNotificationRequest.ProtoReflect.Descriptor instead. func (*CreateGlobalNotificationRequest) Descriptor() ([]byte, []int) { - return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{4} + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{5} } func (x *CreateGlobalNotificationRequest) GetGlobalNotification() *GlobalNotification { @@ -342,7 +418,7 @@ type DeleteGlobalNotificationRequest struct { func (x *DeleteGlobalNotificationRequest) Reset() { *x = DeleteGlobalNotificationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[5] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +431,7 @@ func (x *DeleteGlobalNotificationRequest) String() string { func (*DeleteGlobalNotificationRequest) ProtoMessage() {} func (x *DeleteGlobalNotificationRequest) ProtoReflect() protoreflect.Message { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[5] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +444,7 @@ func (x *DeleteGlobalNotificationRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteGlobalNotificationRequest.ProtoReflect.Descriptor instead. func (*DeleteGlobalNotificationRequest) Descriptor() ([]byte, []int) { - return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{5} + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{6} } func (x *DeleteGlobalNotificationRequest) GetNotificationId() string { @@ -393,7 +469,7 @@ type UpsertUserNotificationStateRequest struct { func (x *UpsertUserNotificationStateRequest) Reset() { *x = UpsertUserNotificationStateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[6] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -406,7 +482,7 @@ func (x *UpsertUserNotificationStateRequest) String() string { func (*UpsertUserNotificationStateRequest) ProtoMessage() {} func (x *UpsertUserNotificationStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[6] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -419,7 +495,7 @@ func (x *UpsertUserNotificationStateRequest) ProtoReflect() protoreflect.Message // Deprecated: Use UpsertUserNotificationStateRequest.ProtoReflect.Descriptor instead. func (*UpsertUserNotificationStateRequest) Descriptor() ([]byte, []int) { - return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{6} + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{7} } func (x *UpsertUserNotificationStateRequest) GetUsername() string { @@ -451,7 +527,7 @@ type UpsertUserLastSeenNotificationRequest struct { func (x *UpsertUserLastSeenNotificationRequest) Reset() { *x = UpsertUserLastSeenNotificationRequest{} if protoimpl.UnsafeEnabled { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[7] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -464,7 +540,7 @@ func (x *UpsertUserLastSeenNotificationRequest) String() string { func (*UpsertUserLastSeenNotificationRequest) ProtoMessage() {} func (x *UpsertUserLastSeenNotificationRequest) ProtoReflect() protoreflect.Message { - mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[7] + mi := &file_teleport_notifications_v1_notifications_service_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -477,7 +553,7 @@ func (x *UpsertUserLastSeenNotificationRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use UpsertUserLastSeenNotificationRequest.ProtoReflect.Descriptor instead. func (*UpsertUserLastSeenNotificationRequest) Descriptor() ([]byte, []int) { - return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{7} + return file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP(), []int{8} } func (x *UpsertUserLastSeenNotificationRequest) GetUsername() string { @@ -524,128 +600,141 @@ var file_teleport_notifications_v1_notifications_service_proto_rawDesc = []byte{ 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x22, 0x56, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, - 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, - 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x80, 0x02, 0x0a, 0x19, 0x4c, 0x69, 0x73, - 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, - 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, - 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x6c, 0x0a, - 0x25, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, - 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x21, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x61, - 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x81, 0x01, 0x0a, 0x1f, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x5e, 0x0a, 0x13, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, - 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x12, 0x67, 0x6c, 0x6f, - 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, - 0x4a, 0x0a, 0x1f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xaa, 0x01, 0x0a, 0x22, - 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 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, 0x12, 0x68, - 0x0a, 0x17, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, - 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x52, 0x15, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x25, 0x55, 0x70, 0x73, - 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x72, - 0x0a, 0x1b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, - 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x18, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x61, - 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x32, 0xa1, 0x07, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x16, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, - 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, + 0x22, 0xa0, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, + 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, + 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x48, 0x0a, 0x07, 0x66, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x73, 0x22, 0x7e, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 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, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, + 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x67, 0x6c, 0x6f, + 0x62, 0x61, 0x6c, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x4f, + 0x6e, 0x6c, 0x79, 0x22, 0x80, 0x02, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x4d, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x6c, 0x0a, 0x25, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x21, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, + 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x81, 0x01, 0x0a, 0x1f, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5e, 0x0a, 0x13, 0x67, 0x6c, + 0x6f, 0x62, 0x61, 0x6c, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x12, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4a, 0x0a, 0x1f, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, + 0x0f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x22, 0xaa, 0x01, 0x0a, 0x22, 0x55, 0x70, 0x73, 0x65, 0x72, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 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, 0x12, 0x68, 0x0a, 0x17, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x15, 0x75, 0x73, + 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x25, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x72, 0x0a, 0x1b, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x6e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x6a, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x85, 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x6c, - 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x3a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4c, + 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x18, 0x75, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, + 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xa1, 0x07, + 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x7b, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x6a, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, + 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, - 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x6e, 0x0a, 0x18, 0x44, - 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x55, + 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x85, + 0x01, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x47, 0x6c, + 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, - 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x7e, 0x0a, 0x11, 0x4c, - 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, + 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x6e, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x3a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0x12, 0x7e, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, + 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x1b, 0x55, 0x70, 0x73, 0x65, 0x72, + 0x74, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8e, 0x01, 0x0a, 0x1b, - 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3d, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, - 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x97, 0x01, 0x0a, - 0x1e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, - 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x40, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, + 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x97, 0x01, 0x0a, 0x1e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, - 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x5e, 0x5a, 0x5c, 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, 0x74, 0x65, 0x6c, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, + 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x42, 0x5e, 0x5a, 0x5c, 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, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, + 0x31, 0x3b, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x76, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -660,49 +749,51 @@ func file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP() [] return file_teleport_notifications_v1_notifications_service_proto_rawDescData } -var file_teleport_notifications_v1_notifications_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_teleport_notifications_v1_notifications_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_teleport_notifications_v1_notifications_service_proto_goTypes = []interface{}{ (*CreateUserNotificationRequest)(nil), // 0: teleport.notifications.v1.CreateUserNotificationRequest (*DeleteUserNotificationRequest)(nil), // 1: teleport.notifications.v1.DeleteUserNotificationRequest (*ListNotificationsRequest)(nil), // 2: teleport.notifications.v1.ListNotificationsRequest - (*ListNotificationsResponse)(nil), // 3: teleport.notifications.v1.ListNotificationsResponse - (*CreateGlobalNotificationRequest)(nil), // 4: teleport.notifications.v1.CreateGlobalNotificationRequest - (*DeleteGlobalNotificationRequest)(nil), // 5: teleport.notifications.v1.DeleteGlobalNotificationRequest - (*UpsertUserNotificationStateRequest)(nil), // 6: teleport.notifications.v1.UpsertUserNotificationStateRequest - (*UpsertUserLastSeenNotificationRequest)(nil), // 7: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest - (*Notification)(nil), // 8: teleport.notifications.v1.Notification - (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp - (*GlobalNotification)(nil), // 10: teleport.notifications.v1.GlobalNotification - (*UserNotificationState)(nil), // 11: teleport.notifications.v1.UserNotificationState - (*UserLastSeenNotification)(nil), // 12: teleport.notifications.v1.UserLastSeenNotification - (*emptypb.Empty)(nil), // 13: google.protobuf.Empty + (*NotificationFilters)(nil), // 3: teleport.notifications.v1.NotificationFilters + (*ListNotificationsResponse)(nil), // 4: teleport.notifications.v1.ListNotificationsResponse + (*CreateGlobalNotificationRequest)(nil), // 5: teleport.notifications.v1.CreateGlobalNotificationRequest + (*DeleteGlobalNotificationRequest)(nil), // 6: teleport.notifications.v1.DeleteGlobalNotificationRequest + (*UpsertUserNotificationStateRequest)(nil), // 7: teleport.notifications.v1.UpsertUserNotificationStateRequest + (*UpsertUserLastSeenNotificationRequest)(nil), // 8: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest + (*Notification)(nil), // 9: teleport.notifications.v1.Notification + (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*GlobalNotification)(nil), // 11: teleport.notifications.v1.GlobalNotification + (*UserNotificationState)(nil), // 12: teleport.notifications.v1.UserNotificationState + (*UserLastSeenNotification)(nil), // 13: teleport.notifications.v1.UserLastSeenNotification + (*emptypb.Empty)(nil), // 14: google.protobuf.Empty } var file_teleport_notifications_v1_notifications_service_proto_depIdxs = []int32{ - 8, // 0: teleport.notifications.v1.CreateUserNotificationRequest.notification:type_name -> teleport.notifications.v1.Notification - 8, // 1: teleport.notifications.v1.ListNotificationsResponse.notifications:type_name -> teleport.notifications.v1.Notification - 9, // 2: teleport.notifications.v1.ListNotificationsResponse.user_last_seen_notification_timestamp:type_name -> google.protobuf.Timestamp - 10, // 3: teleport.notifications.v1.CreateGlobalNotificationRequest.global_notification:type_name -> teleport.notifications.v1.GlobalNotification - 11, // 4: teleport.notifications.v1.UpsertUserNotificationStateRequest.user_notification_state:type_name -> teleport.notifications.v1.UserNotificationState - 12, // 5: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest.user_last_seen_notification:type_name -> teleport.notifications.v1.UserLastSeenNotification - 0, // 6: teleport.notifications.v1.NotificationService.CreateUserNotification:input_type -> teleport.notifications.v1.CreateUserNotificationRequest - 1, // 7: teleport.notifications.v1.NotificationService.DeleteUserNotification:input_type -> teleport.notifications.v1.DeleteUserNotificationRequest - 4, // 8: teleport.notifications.v1.NotificationService.CreateGlobalNotification:input_type -> teleport.notifications.v1.CreateGlobalNotificationRequest - 5, // 9: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:input_type -> teleport.notifications.v1.DeleteGlobalNotificationRequest - 2, // 10: teleport.notifications.v1.NotificationService.ListNotifications:input_type -> teleport.notifications.v1.ListNotificationsRequest - 6, // 11: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:input_type -> teleport.notifications.v1.UpsertUserNotificationStateRequest - 7, // 12: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:input_type -> teleport.notifications.v1.UpsertUserLastSeenNotificationRequest - 8, // 13: teleport.notifications.v1.NotificationService.CreateUserNotification:output_type -> teleport.notifications.v1.Notification - 13, // 14: teleport.notifications.v1.NotificationService.DeleteUserNotification:output_type -> google.protobuf.Empty - 10, // 15: teleport.notifications.v1.NotificationService.CreateGlobalNotification:output_type -> teleport.notifications.v1.GlobalNotification - 13, // 16: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:output_type -> google.protobuf.Empty - 3, // 17: teleport.notifications.v1.NotificationService.ListNotifications:output_type -> teleport.notifications.v1.ListNotificationsResponse - 11, // 18: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:output_type -> teleport.notifications.v1.UserNotificationState - 12, // 19: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:output_type -> teleport.notifications.v1.UserLastSeenNotification - 13, // [13:20] is the sub-list for method output_type - 6, // [6:13] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 9, // 0: teleport.notifications.v1.CreateUserNotificationRequest.notification:type_name -> teleport.notifications.v1.Notification + 3, // 1: teleport.notifications.v1.ListNotificationsRequest.filters:type_name -> teleport.notifications.v1.NotificationFilters + 9, // 2: teleport.notifications.v1.ListNotificationsResponse.notifications:type_name -> teleport.notifications.v1.Notification + 10, // 3: teleport.notifications.v1.ListNotificationsResponse.user_last_seen_notification_timestamp:type_name -> google.protobuf.Timestamp + 11, // 4: teleport.notifications.v1.CreateGlobalNotificationRequest.global_notification:type_name -> teleport.notifications.v1.GlobalNotification + 12, // 5: teleport.notifications.v1.UpsertUserNotificationStateRequest.user_notification_state:type_name -> teleport.notifications.v1.UserNotificationState + 13, // 6: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest.user_last_seen_notification:type_name -> teleport.notifications.v1.UserLastSeenNotification + 0, // 7: teleport.notifications.v1.NotificationService.CreateUserNotification:input_type -> teleport.notifications.v1.CreateUserNotificationRequest + 1, // 8: teleport.notifications.v1.NotificationService.DeleteUserNotification:input_type -> teleport.notifications.v1.DeleteUserNotificationRequest + 5, // 9: teleport.notifications.v1.NotificationService.CreateGlobalNotification:input_type -> teleport.notifications.v1.CreateGlobalNotificationRequest + 6, // 10: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:input_type -> teleport.notifications.v1.DeleteGlobalNotificationRequest + 2, // 11: teleport.notifications.v1.NotificationService.ListNotifications:input_type -> teleport.notifications.v1.ListNotificationsRequest + 7, // 12: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:input_type -> teleport.notifications.v1.UpsertUserNotificationStateRequest + 8, // 13: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:input_type -> teleport.notifications.v1.UpsertUserLastSeenNotificationRequest + 9, // 14: teleport.notifications.v1.NotificationService.CreateUserNotification:output_type -> teleport.notifications.v1.Notification + 14, // 15: teleport.notifications.v1.NotificationService.DeleteUserNotification:output_type -> google.protobuf.Empty + 11, // 16: teleport.notifications.v1.NotificationService.CreateGlobalNotification:output_type -> teleport.notifications.v1.GlobalNotification + 14, // 17: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:output_type -> google.protobuf.Empty + 4, // 18: teleport.notifications.v1.NotificationService.ListNotifications:output_type -> teleport.notifications.v1.ListNotificationsResponse + 12, // 19: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:output_type -> teleport.notifications.v1.UserNotificationState + 13, // 20: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:output_type -> teleport.notifications.v1.UserLastSeenNotification + 14, // [14:21] is the sub-list for method output_type + 7, // [7:14] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_teleport_notifications_v1_notifications_service_proto_init() } @@ -749,7 +840,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { } } file_teleport_notifications_v1_notifications_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListNotificationsResponse); i { + switch v := v.(*NotificationFilters); i { case 0: return &v.state case 1: @@ -761,7 +852,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { } } file_teleport_notifications_v1_notifications_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CreateGlobalNotificationRequest); i { + switch v := v.(*ListNotificationsResponse); i { case 0: return &v.state case 1: @@ -773,7 +864,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { } } file_teleport_notifications_v1_notifications_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeleteGlobalNotificationRequest); i { + switch v := v.(*CreateGlobalNotificationRequest); i { case 0: return &v.state case 1: @@ -785,7 +876,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { } } file_teleport_notifications_v1_notifications_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpsertUserNotificationStateRequest); i { + switch v := v.(*DeleteGlobalNotificationRequest); i { case 0: return &v.state case 1: @@ -797,6 +888,18 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { } } file_teleport_notifications_v1_notifications_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpsertUserNotificationStateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_teleport_notifications_v1_notifications_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UpsertUserLastSeenNotificationRequest); i { case 0: return &v.state @@ -815,7 +918,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_notifications_v1_notifications_service_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/api/proto/teleport/notifications/v1/notifications_service.proto b/api/proto/teleport/notifications/v1/notifications_service.proto index ce3f53579c3ef..a32a5921ab7e5 100644 --- a/api/proto/teleport/notifications/v1/notifications_service.proto +++ b/api/proto/teleport/notifications/v1/notifications_service.proto @@ -70,6 +70,18 @@ message ListNotificationsRequest { int32 page_size = 1; // page_token is the next_page_token value returned from a previous ListUserNotifications request, if any. string page_token = 2; + // filters specify search criteria to limit which notifications should be returned. If omitted, the default behavior will be to list all notifications. + NotificationFilters filters = 3; +} + +// NotificationFilters provide a mechanism to refine ListNotification results. +message NotificationFilters { + // username is the username of the user the notifications being listed are for. + string username = 1; + // global_only is whether to only list global notifications (notifications capable of targetting multiple users). + bool global_only = 2; + // user_created_only is whether to only list user-created notifications (ie. notifications created by an admin via the tctl interface). + bool user_created_only = 3; } // ListNotificationsResponse is the response from listing a user's notifications. diff --git a/api/types/constants.go b/api/types/constants.go index 4c3584e825513..e7f3bae75a657 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1038,6 +1038,8 @@ const ( NotificationClickedLabel = TeleportInternalLabelPrefix + "clicked" // NotificationScope is the label which contains the scope of the notification, either "user" or "global" NotificationScope = TeleportInternalLabelPrefix + "scope" + // NotificationTextContentLabel is the label which contains the text content of a user-created notification. + NotificationTextContentLabel = TeleportInternalLabelPrefix + "content" // NotificationDefaultInformationalSubKind is the default subkind for an informational notification. NotificationDefaultInformationalSubKind = "default-informational" diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go index 0fbf9115f425c..2547b8884e299 100644 --- a/lib/auth/authclient/clt.go +++ b/lib/auth/authclient/clt.go @@ -712,21 +712,36 @@ func (c *Client) UpsertUserLastSeenNotification(ctx context.Context, username st } // CreateGlobalNotification creates a global notification. -func (c *Client) CreateGlobalNotification(ctx context.Context, globalNotification *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, error) { - // TODO(rudream): implement client methods for notifications - return nil, trace.NotImplemented(notImplementedMessage) +func (c *Client) CreateGlobalNotification(ctx context.Context, gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, error) { + rsp, err := c.APIClient.CreateGlobalNotification(ctx, ¬ificationsv1.CreateGlobalNotificationRequest{ + GlobalNotification: gn, + }) + return rsp, trace.Wrap(err) } // CreateUserNotification creates a user-specific notification. func (c *Client) CreateUserNotification(ctx context.Context, notification *notificationsv1.Notification) (*notificationsv1.Notification, error) { - // TODO(rudream): implement client methods for notifications - return nil, trace.NotImplemented(notImplementedMessage) + rsp, err := c.APIClient.CreateUserNotification(ctx, ¬ificationsv1.CreateUserNotificationRequest{ + Notification: notification, + }) + return rsp, trace.Wrap(err) } // DeleteGlobalNotification deletes a global notification. func (c *Client) DeleteGlobalNotification(ctx context.Context, notificationId string) error { - // TODO(rudream): implement client methods for notifications - return trace.NotImplemented(notImplementedMessage) + err := c.APIClient.DeleteGlobalNotification(ctx, ¬ificationsv1.DeleteGlobalNotificationRequest{ + NotificationId: notificationId, + }) + return trace.Wrap(err) +} + +// DeleteUserNotification not implemented: can only be called locally. +func (c *Client) DeleteUserNotification(ctx context.Context, username string, notificationId string) error { + err := c.APIClient.DeleteUserNotification(ctx, ¬ificationsv1.DeleteUserNotificationRequest{ + Username: username, + NotificationId: notificationId, + }) + return trace.Wrap(err) } // DeleteAllGlobalNotifications not implemented: can only be called locally. @@ -754,11 +769,6 @@ func (c *Client) DeleteUserLastSeenNotification(ctx context.Context, username st return trace.NotImplemented(notImplementedMessage) } -// DeleteUserNotification not implemented: can only be called locally. -func (c *Client) DeleteUserNotification(ctx context.Context, username string, notificationId string) error { - return trace.NotImplemented(notImplementedMessage) -} - // DeleteUserNotificationState not implemented: can only be called locally. func (c *Client) DeleteUserNotificationState(ctx context.Context, username string, notificationId string) error { return trace.NotImplemented(notImplementedMessage) diff --git a/lib/auth/notifications/notificationsv1/service.go b/lib/auth/notifications/notificationsv1/service.go index d62976f59d145..7edcbbe337e1c 100644 --- a/lib/auth/notifications/notificationsv1/service.go +++ b/lib/auth/notifications/notificationsv1/service.go @@ -25,6 +25,7 @@ import ( "github.com/gravitational/trace" "github.com/jonboulle/clockwork" + "google.golang.org/protobuf/types/known/emptypb" "github.com/gravitational/teleport/api/client" apidefaults "github.com/gravitational/teleport/api/defaults" @@ -104,6 +105,17 @@ func NewService(cfg ServiceConfig) (*Service, error) { // ListNotifications returns a paginated list of notifications which match the user. func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) { + if req.Filters != nil { + if req.Filters.GlobalOnly { + return s.listGlobalNotifications(ctx, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize) + } + if req.Filters.Username != "" { + return s.listUserSpecificNotificationsForUser(ctx, req.Filters.Username, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize) + } + + return nil, trace.BadParameter("Invalid filters were provided, exactly one of GlobalOnly or Username must be defined.") + } + authCtx, err := s.authorizer.Authorize(ctx) if err != nil { return nil, trace.Wrap(err) @@ -479,3 +491,191 @@ func (s *Service) UpsertUserLastSeenNotification(ctx context.Context, req *notif return out, nil } + +// CreateGlobalNotification creates a global notification. +func (s *Service) CreateGlobalNotification(ctx context.Context, req *notificationsv1.CreateGlobalNotificationRequest) (*notificationsv1.GlobalNotification, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbCreate); err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + out, err := s.backend.CreateGlobalNotification(ctx, req.GlobalNotification) + if err != nil { + return nil, trace.Wrap(err) + } + + return out, nil +} + +// CreateUserNotification creates a user-specific notification. +func (s *Service) CreateUserNotification(ctx context.Context, req *notificationsv1.CreateUserNotificationRequest) (*notificationsv1.Notification, error) { + if req.Username == "" { + return nil, trace.BadParameter("missing username") + } + + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbCreate); err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + out, err := s.backend.CreateUserNotification(ctx, req.Notification) + if err != nil { + return nil, trace.Wrap(err) + } + + return out, nil +} + +// DeleteGlobalNotification deletes a global notification. +func (s *Service) DeleteGlobalNotification(ctx context.Context, req *notificationsv1.DeleteGlobalNotificationRequest) (*emptypb.Empty, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbDelete); err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + err = s.backend.DeleteGlobalNotification(ctx, req.NotificationId) + return nil, trace.Wrap(err) +} + +// DeleteUserNotification deletes a user-specific notification. +func (s *Service) DeleteUserNotification(ctx context.Context, req *notificationsv1.DeleteUserNotificationRequest) (*emptypb.Empty, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbDelete); err != nil { + return nil, trace.Wrap(err) + } + + if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil { + return nil, trace.Wrap(err) + } + + err = s.backend.DeleteUserNotification(ctx, req.Username, req.NotificationId) + return nil, trace.Wrap(err) +} + +// listUserSpecificNotificationsForUser returns a paginated list of all user-specific notifications for a user. This should only be used by admins. +func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, username string, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) { + if username == "" { + return nil, trace.BadParameter("missing username") + } + + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) { + return nil, trace.AccessDenied("only RoleAdmin can list notifications for a specific user") + } + + if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbList); err != nil { + return nil, trace.Wrap(err) + } + + stream := stream.FilterMap( + s.userNotificationCache.StreamUserNotifications(ctx, username, pageToken), + func(n *notificationsv1.Notification) (*notificationsv1.Notification, bool) { + // If only user-created notifications are requested, filter by the user-creatd subkinds. + if userCreatedOnly && + n.GetSubKind() != types.NotificationUserCreatedInformationalSubKind && + n.GetSubKind() != types.NotificationUserCreatedWarningSubKind { + return nil, false + } + + return n, true + }) + + var notifications []*notificationsv1.Notification + var nextKey string + + for stream.Next() { + item := stream.Item() + if item != nil { + notifications = append(notifications, item) + if len(notifications) == int(pageSize) { + nextKey = item.GetMetadata().GetName() + break + } + } + } + + return ¬ificationsv1.ListNotificationsResponse{ + Notifications: notifications, + NextPageToken: nextKey, + }, nil +} + +// listGlobalNotifications returns a paginated list of all global notifications. This should only be used by admins. +func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) { + authCtx, err := s.authorizer.Authorize(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + if !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) { + return nil, trace.AccessDenied("only RoleAdmin can list all global notifications") + } + + stream := stream.FilterMap( + s.globalNotificationCache.StreamGlobalNotifications(ctx, pageToken), + func(gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, bool) { + // If only user-created notifications are requested, filter by the user-creatd subkinds. + if userCreatedOnly && + gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedInformationalSubKind && + gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedWarningSubKind { + return nil, false + } + + return gn, true + }) + + var notifications []*notificationsv1.Notification + var nextKey string + + for stream.Next() { + item := stream.Item() + if item != nil { + notification := item.GetSpec().GetNotification() + notification.Metadata.Name = item.GetMetadata().GetName() + + notifications = append(notifications, notification) + + if len(notifications) == int(pageSize) { + nextKey = item.GetMetadata().GetName() + break + } + } + } + + return ¬ificationsv1.ListNotificationsResponse{ + Notifications: notifications, + NextPageToken: nextKey, + }, nil +} diff --git a/lib/services/presets.go b/lib/services/presets.go index f66b442934f93..d1ad8236da01c 100644 --- a/lib/services/presets.go +++ b/lib/services/presets.go @@ -177,6 +177,7 @@ func NewPresetEditorRole() types.Role { types.NewRule(types.KindAppServer, RW()), types.NewRule(types.KindVnetConfig, RW()), types.NewRule(types.KindAccessGraphSettings, RW()), + types.NewRule(types.KindNotification, RW()), }, }, }, @@ -282,6 +283,7 @@ func NewPresetAuditorRole() types.Role { types.NewRule(types.KindInstance, RO()), types.NewRule(types.KindSecurityReport, append(RO(), types.VerbUse)), types.NewRule(types.KindAuditQuery, append(RO(), types.VerbUse)), + types.NewRule(types.KindNotification, RO()), }, }, }, diff --git a/lib/web/ui/notification.go b/lib/web/ui/notification.go index c1b8c772d4686..1c75a00a10db5 100644 --- a/lib/web/ui/notification.go +++ b/lib/web/ui/notification.go @@ -26,12 +26,13 @@ import ( ) type Notification struct { - Id string `json:"id"` - Title string `json:"title"` - SubKind string `json:"subKind"` - Created time.Time `json:"created"` - Clicked bool `json:"clicked"` - Labels []Label `json:"labels"` + ID string `json:"id"` + Title string `json:"title"` + SubKind string `json:"subKind"` + Created time.Time `json:"created"` + Clicked bool `json:"clicked"` + TextContent string `json:"textContent,omitempty"` + Labels []Label `json:"labels"` } // MakeNotification creates a notification object for the WebUI. @@ -41,11 +42,12 @@ func MakeNotification(notification *notificationsv1.Notification) Notification { clicked := notification.Metadata.GetLabels()[types.NotificationClickedLabel] == "true" return Notification{ - Id: notification.Metadata.GetName(), - Title: notification.Metadata.GetLabels()[types.NotificationTitleLabel], - SubKind: notification.SubKind, - Created: notification.Spec.Created.AsTime(), - Clicked: clicked, - Labels: labels, + ID: notification.Metadata.GetName(), + Title: notification.Metadata.GetLabels()[types.NotificationTitleLabel], + SubKind: notification.SubKind, + Created: notification.Spec.Created.AsTime(), + Clicked: clicked, + TextContent: notification.Metadata.GetLabels()[types.NotificationTextContentLabel], + Labels: labels, } } diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go index 9a78c4191d28f..6074006e8ef61 100644 --- a/tool/tctl/common/cmds.go +++ b/tool/tctl/common/cmds.go @@ -57,6 +57,7 @@ func Commands() []CLICommand { &IdPCommand{}, &accessmonitoring.Command{}, &plugin.PluginsCommand{}, + &NotificationCommand{}, &configure.SSOConfigureCommand{}, &tester.SSOTestCommand{}, &fido2Command{}, diff --git a/tool/tctl/common/helpers_test.go b/tool/tctl/common/helpers_test.go index 815cb78593d17..a9c17e4cef4fd 100644 --- a/tool/tctl/common/helpers_test.go +++ b/tool/tctl/common/helpers_test.go @@ -133,6 +133,16 @@ func runIdPSAMLCommand(t *testing.T, client *authclient.Client, args []string) e return runCommand(t, client, command, args) } +func runNotificationsCommand(t *testing.T, client *authclient.Client, args []string) (*bytes.Buffer, error) { + var stdoutBuff bytes.Buffer + command := &NotificationCommand{ + stdout: &stdoutBuff, + } + + args = append([]string{"notifications"}, args...) + return &stdoutBuff, runCommand(t, client, command, args) +} + func mustDecodeJSON[T any](t *testing.T, r io.Reader) T { var out T err := json.NewDecoder(r).Decode(&out) diff --git a/tool/tctl/common/notification_command.go b/tool/tctl/common/notification_command.go new file mode 100644 index 0000000000000..640b2e8b32b1c --- /dev/null +++ b/tool/tctl/common/notification_command.go @@ -0,0 +1,313 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + "github.com/gravitational/trace/trail" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/defaults" + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + notificationspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1" + "github.com/gravitational/teleport/api/mfa" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/asciitable" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/utils" +) + +// NotificationCommand implements the `tctl notifications` family of commands. +type NotificationCommand struct { + ls *kingpin.CmdClause + rm *kingpin.CmdClause + create *kingpin.CmdClause + + format string + user string + roles []string + requireAllRoles bool + warning bool + // allNotifications determines whether the list returned will include all notifications and not just those created manually by a user via tctl. + allNotifications bool + + title string + content string + + // stdout allows to switch the standard output source. Used in tests. + stdout io.Writer +} + +// Initialize allows NotificationCommand command to plug itself into the CLI parser +func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *servicecfg.Config) { + notif := app.Command("notifications", "Manage cluster notifications.") + + n.create = notif.Command("create", "Create a cluster notification.").Alias("add") + n.create.Flag("user", "Target a specific user.").StringVar(&n.user) + n.create.Flag("roles", "Target a specific set of roles. By default, this will target all users with any of the provided roles, use --require-all-roles to exclusively target users with all of them.").StringsVar(&n.roles) + n.create.Flag("require-all-roles", "Set whether this notification should target users who have all of the provided roles.").BoolVar(&n.requireAllRoles) + n.create.Flag("title", "Set the notification's title.").Short('t').Required().StringVar(&n.title) + n.create.Flag("content", "Set the notification's content.").Required().StringVar(&n.content) + n.create.Flag("warning", "Set whether this notification is a warning notification.").BoolVar(&n.warning) + + n.ls = notif.Command("ls", "List notifications which were manually created using `tctl notifications create`. By default, this will list notifications capable of targeting multiple users, such as role-based ones. To list notifications directed only at a specific user, use the --user flag. To include notifications generated by Teleport, use --all.") + n.ls.Flag("user", "Set which user to list user-specific notifications for.").StringVar(&n.user) + n.ls.Flag("format", "Output format, 'yaml', 'json', or 'text'").Default(teleport.Text).EnumVar(&n.format, teleport.YAML, teleport.JSON, teleport.Text) + n.ls.Flag("all", "Set whether all notifications should be included, including those generated by Teleport, as opposed to solely those created using `tctl notifications create`.").BoolVar(&n.allNotifications) + + n.rm = notif.Command("rm", "Remove a cluster notification.").Alias("remove") + n.rm.Flag("user", "The user the notification to remove belongs to, if any.").StringVar(&n.user) + n.rm.Arg("id", "The ID of the notification to remove.").Required().StringVar(&n.title) + + if n.stdout == nil { + n.stdout = os.Stdout + } +} + +// TryRun takes the CLI command as an argument and executes it. +func (n *NotificationCommand) TryRun(ctx context.Context, cmd string, client *authclient.Client) (match bool, err error) { + nc := client.NotificationServiceClient() + + switch cmd { + case n.create.FullCommand(): + err = n.Create(ctx, client) + case n.ls.FullCommand(): + err = n.List(ctx, nc) + case n.rm.FullCommand(): + err = n.Remove(ctx, client) + default: + return false, nil + } + return true, trace.Wrap(err) +} + +// Create creates a new notification. +func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Client) error { + meta := &headerv1.Metadata{ + Labels: map[string]string{ + types.NotificationTitleLabel: n.title, + types.NotificationTextContentLabel: n.content, + }, + } + + subKind := types.NotificationUserCreatedInformationalSubKind + if n.warning { + subKind = types.NotificationUserCreatedWarningSubKind + } + + // Prompt for admin action MFA re-auth. + mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/) + if err == nil { + ctx = mfa.ContextWithMFAResponse(ctx, mfaResponse) + } else if !errors.Is(err, &mfa.ErrMFANotRequired) && !errors.Is(err, &mfa.ErrMFANotSupported) { + return trace.Wrap(err) + } + + nc := client.NotificationServiceClient() + + if n.user != "" { + if len(n.roles) != 0 || n.requireAllRoles { + return trace.BadParameter("roles cannot be configured for a notification which targets a specific user") + } + + created, err := nc.CreateUserNotification(ctx, ¬ificationspb.CreateUserNotificationRequest{ + Username: n.user, + Notification: ¬ificationspb.Notification{ + Kind: types.KindNotification, + SubKind: subKind, + Metadata: meta, + Spec: ¬ificationspb.NotificationSpec{ + Username: n.user, + }, + }, + }) + + if err != nil { + return trail.FromGRPC(err) + } + + fmt.Fprintf(n.stdout, "Created notification %s for user %s\n", created.GetMetadata().GetName(), n.user) + return nil + } + + if len(n.roles) != 0 { + created, err := nc.CreateGlobalNotification(ctx, ¬ificationspb.CreateGlobalNotificationRequest{ + GlobalNotification: ¬ificationspb.GlobalNotification{ + Kind: types.KindGlobalNotification, + Spec: ¬ificationspb.GlobalNotificationSpec{ + Matcher: ¬ificationspb.GlobalNotificationSpec_ByRoles{ + ByRoles: ¬ificationspb.ByRoles{ + Roles: n.roles, + }, + }, + MatchAllConditions: n.requireAllRoles, + Notification: ¬ificationspb.Notification{ + Kind: types.KindNotification, + SubKind: subKind, + Metadata: meta, + Spec: ¬ificationspb.NotificationSpec{}, + }, + }, + }, + }) + + if err != nil { + return trail.FromGRPC(err) + } + + if n.requireAllRoles { + fmt.Fprintf(n.stdout, "Created notification %s for users with all of the following roles: %s\n", created.GetMetadata().GetName(), n.roles) + return nil + } + + fmt.Fprintf(n.stdout, "Created notification %s for users with one or more of the following roles: %s\n", created.GetMetadata().GetName(), n.roles) + return nil + } + + if n.requireAllRoles { + return trace.BadParameter("--require-all-roles was set, but no --roles were provided") + } + + // If roles weren't provided, default to targeting all users. + created, err := nc.CreateGlobalNotification(ctx, ¬ificationspb.CreateGlobalNotificationRequest{ + GlobalNotification: ¬ificationspb.GlobalNotification{ + Kind: types.KindGlobalNotification, + Spec: ¬ificationspb.GlobalNotificationSpec{ + Matcher: ¬ificationspb.GlobalNotificationSpec_All{ + All: true, + }, + Notification: ¬ificationspb.Notification{ + Kind: types.KindNotification, + SubKind: subKind, + Metadata: meta, + Spec: ¬ificationspb.NotificationSpec{}, + }, + }, + }, + }) + if err != nil { + return trail.FromGRPC(err) + } + + fmt.Fprintf(n.stdout, "Created notification %s for all users\n", created.GetMetadata().GetName()) + return nil +} + +func (n *NotificationCommand) List(ctx context.Context, client notificationspb.NotificationServiceClient) error { + var result []*notificationspb.Notification + var pageToken string + for { + var resp *notificationspb.ListNotificationsResponse + var err error + + // If a user was specified, list user-specific notifications for them, if not, default to listing global notifications. + if n.user != "" { + resp, err = client.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ + PageSize: defaults.DefaultChunkSize, + PageToken: pageToken, + Filters: ¬ificationspb.NotificationFilters{ + Username: n.user, + UserCreatedOnly: !n.allNotifications, + }, + }) + if err != nil { + return trace.Wrap(err) + } + } else { + resp, err = client.ListNotifications(ctx, ¬ificationspb.ListNotificationsRequest{ + PageSize: defaults.DefaultChunkSize, + PageToken: pageToken, + Filters: ¬ificationspb.NotificationFilters{ + GlobalOnly: true, + UserCreatedOnly: !n.allNotifications, + }, + }) + if err != nil { + return trace.Wrap(err) + } + } + + result = append(result, resp.Notifications...) + pageToken = resp.GetNextPageToken() + if pageToken == "" { + break + } + } + + displayNotifications(n.format, result, n.stdout) + return nil +} + +func displayNotifications(format string, notifications []*notificationspb.Notification, w io.Writer) { + switch format { + case teleport.Text: + table := asciitable.MakeTable([]string{"ID", "Created", "Expires", "Title"}) + for _, n := range notifications { + table.AddRow([]string{ + n.GetMetadata().GetName(), + n.GetSpec().GetCreated().AsTime().Format(time.RFC822), + n.GetMetadata().GetExpires().AsTime().Format(time.RFC822), + n.GetMetadata().GetLabels()[types.NotificationTitleLabel], + }) + } + fmt.Fprint(w, table.AsBuffer().String()) + case teleport.JSON: + utils.WriteJSONArray(w, notifications) + case teleport.YAML: + utils.WriteYAML(w, notifications) + default: + // Do nothing, kingpin validates the --format flag before we ever get here. + } +} + +// Remove removes a notification. +func (n *NotificationCommand) Remove(ctx context.Context, client *authclient.Client) error { + // Prompt for admin action MFA re-auth. + mfaResponse, err := mfa.PerformAdminActionMFACeremony(ctx, client.PerformMFACeremony, true /*allowReuse*/) + if err == nil { + ctx = mfa.ContextWithMFAResponse(ctx, mfaResponse) + } else if !errors.Is(err, &mfa.ErrMFANotRequired) && !errors.Is(err, &mfa.ErrMFANotSupported) { + return trace.Wrap(err) + } + + nc := client.NotificationServiceClient() + + switch { + case n.user != "": + _, err = nc.DeleteUserNotification(ctx, ¬ificationspb.DeleteUserNotificationRequest{ + Username: n.user, + NotificationId: n.title, + }) + default: + _, err = nc.DeleteGlobalNotification(ctx, ¬ificationspb.DeleteGlobalNotificationRequest{ + NotificationId: n.title, + }) + } + + return trail.FromGRPC(err) +} diff --git a/tool/tctl/common/notification_command_test.go b/tool/tctl/common/notification_command_test.go new file mode 100644 index 0000000000000..9309e45bfd727 --- /dev/null +++ b/tool/tctl/common/notification_command_test.go @@ -0,0 +1,113 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/integration/helpers" + "github.com/gravitational/teleport/lib/config" + "github.com/gravitational/teleport/tool/teleport/testenv" +) + +// TestNotificationCommandCRUD tests creating, listing, and deleting notifications via the `tctl notifications` commands. +func TestNotificationCommmandCRUD(t *testing.T) { + dynAddr := helpers.NewDynamicServiceAddr(t) + fileConfig := &config.FileConfig{ + Global: config.Global{ + DataDir: t.TempDir(), + }, + Auth: config.Auth{ + Service: config.Service{ + EnabledFlag: "true", + ListenAddress: dynAddr.AuthAddr, + }, + }, + } + process := makeAndRunTestAuthServer(t, withFileConfig(fileConfig), withFileDescriptors(dynAddr.Descriptors)) + + clt := testenv.MakeDefaultAuthClient(t, process) + + auditorUsername := "auditor-user" + managerUsername := "manager-user" + + // Test creating a user-specific notification for auditor user. + buf, err := runNotificationsCommand(t, clt, []string{"create", "--user", auditorUsername, "--title", "auditor notification", "--content", "This is a test notification."}) + require.NoError(t, err) + require.Contains(t, buf.String(), "for user auditor-user") + auditorUserNotificationId := strings.Split(buf.String(), " ")[2] + + // Test creating a user-specific notification for manager user. + buf, err = runNotificationsCommand(t, clt, []string{"create", "--user", managerUsername, "--title", "manager notification", "--content", "This is a test notification."}) + require.NoError(t, err) + require.Contains(t, buf.String(), "for user manager-user") + + // Test creating a global notification for users with the test-1 role. + buf, err = runNotificationsCommand(t, clt, []string{"create", "--roles", "test-1", "--title", "test-1 notification", "--content", "This is a test notification."}) + require.NoError(t, err) + require.Contains(t, buf.String(), "for users with one or more of the following roles: [test-1]") + globalNotificationId := strings.Split(buf.String(), " ")[2] + + // We periodically check with a timeout since it can take some time for the item to be replicated in the cache and be available for listing. + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + // List notifications for auditor and verify that auditor notification exists. + buf, err = runNotificationsCommand(t, clt, []string{"ls", "--user", auditorUsername}) + assert.NoError(collectT, err) + assert.Contains(collectT, buf.String(), "auditor notification") + assert.NotContains(collectT, buf.String(), "manager notification") + + // List notifications for manager and verify output. + buf, err = runNotificationsCommand(t, clt, []string{"ls", "--user", managerUsername}) + assert.NoError(collectT, err) + assert.Contains(collectT, buf.String(), "manager notification") + assert.NotContains(collectT, buf.String(), "auditor notification") + + // List global notifications and verify that test-1 notification exists. + buf, err = runNotificationsCommand(t, clt, []string{"ls"}) + assert.NoError(collectT, err) + assert.Contains(collectT, buf.String(), "test-1 notification") + assert.NotContains(collectT, buf.String(), "auditor notification") + assert.NotContains(collectT, buf.String(), "manager notification") + + }, 3*time.Second, 100*time.Millisecond) + + // Delete the auditor's user-specific notification. + _, err = runNotificationsCommand(t, clt, []string{"rm", auditorUserNotificationId, "--user", auditorUsername}) + require.NoError(t, err) + // Delete the global notification. + _, err = runNotificationsCommand(t, clt, []string{"rm", globalNotificationId}) + require.NoError(t, err) + + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + // Verify that the global notification is no longer listed. + buf, err = runNotificationsCommand(t, clt, []string{"ls"}) + assert.NoError(collectT, err) + assert.NotContains(collectT, buf.String(), "test-1 notification") + + // Verify that the auditor notification is no longer listed. + buf, err = runNotificationsCommand(t, clt, []string{"ls", "--user", auditorUsername}) + assert.NoError(collectT, err) + assert.NotContains(collectT, buf.String(), "auditor notification") + }, 3*time.Second, 100*time.Millisecond) +} diff --git a/web/packages/teleport/src/Notifications/Notification.tsx b/web/packages/teleport/src/Notifications/Notification.tsx index 8398b2b01442d..ee4a0c93b5bdf 100644 --- a/web/packages/teleport/src/Notifications/Notification.tsx +++ b/web/packages/teleport/src/Notifications/Notification.tsx @@ -107,6 +107,27 @@ export function Notification({ // If the notification is unsupported or hidden, or if the view is "Unread" and the notification has been read, // it should not be shown. if (!content || (view === 'Unread' && notification.clicked)) { + // If this is a text content notification, the dialog should still be renderable. This is to prevent the text content dialog immediately disappearing + // when trying to open an unread text notification, since clicking on the notification instantly marks it as read. + if (content.kind == 'text') { + return ( + + + {content.title} + + {content.textContent} + + setShowTextContentDialog(false)} + size="small" + className={IGNORE_CLICK_CLASSNAME} + > + Close + + + + ); + } return null; } @@ -135,6 +156,7 @@ export function Notification({ if (e.currentTarget.contains(e.target as HTMLElement)) { if (content.kind === 'text') { setShowTextContentDialog(true); + onMarkAsClicked(); return; } onMarkAsClicked(); diff --git a/web/packages/teleport/src/Notifications/notificationContentFactory.tsx b/web/packages/teleport/src/Notifications/notificationContentFactory.tsx index bebf8ae862bf8..6b453357acd90 100644 --- a/web/packages/teleport/src/Notifications/notificationContentFactory.tsx +++ b/web/packages/teleport/src/Notifications/notificationContentFactory.tsx @@ -31,7 +31,6 @@ import { Label } from 'teleport/types'; */ export function notificationContentFactory({ subKind, - labels, ...notification }: NotificationType): NotificationContent { let notificationContent: NotificationContent; @@ -39,11 +38,10 @@ export function notificationContentFactory({ switch (subKind) { case NotificationSubKind.DefaultInformational: case NotificationSubKind.UserCreatedInformational: { - const textContent = getLabelValue(labels, 'content'); notificationContent = { kind: 'text', title: notification.title, - textContent, + textContent: notification.textContent, type: 'informational', icon: Icons.Notification, }; @@ -52,11 +50,10 @@ export function notificationContentFactory({ case NotificationSubKind.DefaultWarning: case NotificationSubKind.UserCreatedWarning: { - const textContent = getLabelValue(labels, 'content'); notificationContent = { kind: 'text', title: notification.title, - textContent, + textContent: notification.textContent, type: 'warning', icon: Icons.Notification, }; diff --git a/web/packages/teleport/src/services/notifications/notifications.ts b/web/packages/teleport/src/services/notifications/notifications.ts index ed581e5b4365b..64ef809bb34dc 100644 --- a/web/packages/teleport/src/services/notifications/notifications.ts +++ b/web/packages/teleport/src/services/notifications/notifications.ts @@ -37,7 +37,8 @@ export class NotificationService { return { notifications: json.notifications ? json.notifications.map(notificationJson => { - const { id, title, subKind, created, clicked } = notificationJson; + const { id, title, subKind, created, clicked, textContent } = + notificationJson; const labels = notificationJson.labels || []; return { @@ -47,6 +48,7 @@ export class NotificationService { createdDate: new Date(created), clicked, labels, + textContent, }; }) : [], diff --git a/web/packages/teleport/src/services/notifications/types.ts b/web/packages/teleport/src/services/notifications/types.ts index 9573089193183..328288b3732db 100644 --- a/web/packages/teleport/src/services/notifications/types.ts +++ b/web/packages/teleport/src/services/notifications/types.ts @@ -77,6 +77,10 @@ export type Notification = { labels: Label[]; /** title is the title of this notification. This can be overwritten in notificationContentFactory if needed. */ title: string; + /** textContent is the text content of this notification if it is merely a text notification (such as one created via `tctl notifications create`). + * This is the text that will be displayed in a dialog upon clicking the notification. + */ + textContent?: string; /** localNotification is whether this is a notification stored in a frontend store as opposed to a "real" notification * from the notifications system. The reason for this is that some notification types (such as access lists) are not supported * by the backend notifications system, and are instead generated entirely on the frontend.