From 2298901e61875f532da377f849a76e3059bf089f Mon Sep 17 00:00:00 2001 From: Zac Bergquist Date: Wed, 13 Nov 2024 10:24:38 -0700 Subject: [PATCH] Enhance the tctl notifications command (#48829) - Add the ability create notifications with a custom TTL (previously we relied on the server-provided default of 30 days) - Add the ability to create notifications with custom labels - Add (non-system) labels to the `tctl notifications ls` output - Add the ability to filter by label when listing notifications Closes #48631 --- .../v1/notifications_service.pb.go | 104 +++++++++++------- .../v1/notifications_service.proto | 2 + .../notifications/notificationsv1/service.go | 60 ++++++++-- tool/tctl/common/notification_command.go | 32 +++++- tool/tctl/common/notification_command_test.go | 18 ++- 5 files changed, 156 insertions(+), 60 deletions(-) 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 6aa44e60fbcae..76d63df78a23d 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 @@ -228,6 +228,8 @@ type NotificationFilters struct { 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"` + // labels is used to request only notifications with specific labels. + Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } func (x *NotificationFilters) Reset() { @@ -281,6 +283,13 @@ func (x *NotificationFilters) GetUserCreatedOnly() bool { return false } +func (x *NotificationFilters) GetLabels() map[string]string { + if x != nil { + return x.Labels + } + return nil +} + // ListNotificationsResponse is the response from listing a user's notifications. type ListNotificationsResponse struct { state protoimpl.MessageState @@ -592,15 +601,24 @@ var file_teleport_notifications_v1_notifications_service_proto_rawDesc = []byte{ 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, + 0x65, 0x72, 0x73, 0x22, 0x8d, 0x02, 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, 0x12, 0x52, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04, + 0x20, 0x03, 0x28, 0x0b, 0x32, 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, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, + 0x74, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 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, @@ -731,7 +749,7 @@ 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, 9) +var file_teleport_notifications_v1_notifications_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_teleport_notifications_v1_notifications_service_proto_goTypes = []any{ (*CreateUserNotificationRequest)(nil), // 0: teleport.notifications.v1.CreateUserNotificationRequest (*DeleteUserNotificationRequest)(nil), // 1: teleport.notifications.v1.DeleteUserNotificationRequest @@ -742,40 +760,42 @@ var file_teleport_notifications_v1_notifications_service_proto_goTypes = []any{ (*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 + nil, // 9: teleport.notifications.v1.NotificationFilters.LabelsEntry + (*Notification)(nil), // 10: teleport.notifications.v1.Notification + (*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp + (*GlobalNotification)(nil), // 12: teleport.notifications.v1.GlobalNotification + (*UserNotificationState)(nil), // 13: teleport.notifications.v1.UserNotificationState + (*UserLastSeenNotification)(nil), // 14: teleport.notifications.v1.UserLastSeenNotification + (*emptypb.Empty)(nil), // 15: google.protobuf.Empty } var file_teleport_notifications_v1_notifications_service_proto_depIdxs = []int32{ - 9, // 0: teleport.notifications.v1.CreateUserNotificationRequest.notification:type_name -> teleport.notifications.v1.Notification + 10, // 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 + 9, // 2: teleport.notifications.v1.NotificationFilters.labels:type_name -> teleport.notifications.v1.NotificationFilters.LabelsEntry + 10, // 3: teleport.notifications.v1.ListNotificationsResponse.notifications:type_name -> teleport.notifications.v1.Notification + 11, // 4: teleport.notifications.v1.ListNotificationsResponse.user_last_seen_notification_timestamp:type_name -> google.protobuf.Timestamp + 12, // 5: teleport.notifications.v1.CreateGlobalNotificationRequest.global_notification:type_name -> teleport.notifications.v1.GlobalNotification + 13, // 6: teleport.notifications.v1.UpsertUserNotificationStateRequest.user_notification_state:type_name -> teleport.notifications.v1.UserNotificationState + 14, // 7: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest.user_last_seen_notification:type_name -> teleport.notifications.v1.UserLastSeenNotification + 0, // 8: teleport.notifications.v1.NotificationService.CreateUserNotification:input_type -> teleport.notifications.v1.CreateUserNotificationRequest + 1, // 9: teleport.notifications.v1.NotificationService.DeleteUserNotification:input_type -> teleport.notifications.v1.DeleteUserNotificationRequest + 5, // 10: teleport.notifications.v1.NotificationService.CreateGlobalNotification:input_type -> teleport.notifications.v1.CreateGlobalNotificationRequest + 6, // 11: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:input_type -> teleport.notifications.v1.DeleteGlobalNotificationRequest + 2, // 12: teleport.notifications.v1.NotificationService.ListNotifications:input_type -> teleport.notifications.v1.ListNotificationsRequest + 7, // 13: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:input_type -> teleport.notifications.v1.UpsertUserNotificationStateRequest + 8, // 14: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:input_type -> teleport.notifications.v1.UpsertUserLastSeenNotificationRequest + 10, // 15: teleport.notifications.v1.NotificationService.CreateUserNotification:output_type -> teleport.notifications.v1.Notification + 15, // 16: teleport.notifications.v1.NotificationService.DeleteUserNotification:output_type -> google.protobuf.Empty + 12, // 17: teleport.notifications.v1.NotificationService.CreateGlobalNotification:output_type -> teleport.notifications.v1.GlobalNotification + 15, // 18: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:output_type -> google.protobuf.Empty + 4, // 19: teleport.notifications.v1.NotificationService.ListNotifications:output_type -> teleport.notifications.v1.ListNotificationsResponse + 13, // 20: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:output_type -> teleport.notifications.v1.UserNotificationState + 14, // 21: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:output_type -> teleport.notifications.v1.UserLastSeenNotification + 15, // [15:22] is the sub-list for method output_type + 8, // [8:15] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_teleport_notifications_v1_notifications_service_proto_init() } @@ -790,7 +810,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: 9, + NumMessages: 10, 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 a32a5921ab7e5..4edcbf5362789 100644 --- a/api/proto/teleport/notifications/v1/notifications_service.proto +++ b/api/proto/teleport/notifications/v1/notifications_service.proto @@ -82,6 +82,8 @@ message NotificationFilters { 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; + // labels is used to request only notifications with specific labels. + map labels = 4; } // ListNotificationsResponse is the response from listing a user's notifications. diff --git a/lib/auth/notifications/notificationsv1/service.go b/lib/auth/notifications/notificationsv1/service.go index 65c5dfef32f6a..49a75548ad4e0 100644 --- a/lib/auth/notifications/notificationsv1/service.go +++ b/lib/auth/notifications/notificationsv1/service.go @@ -107,12 +107,25 @@ 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) { + labelsMatch := func(resourceLabels map[string]string) bool { + if req.Filters == nil || len(req.Filters.Labels) == 0 { + // no labels to match against + return true + } + for k, v := range req.Filters.Labels { + if resourceLabels[k] != v { + return false + } + } + return true + } + if req.Filters != nil { if req.Filters.GlobalOnly { - return s.listGlobalNotifications(ctx, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize) + return s.listGlobalNotifications(ctx, req) } if req.Filters.Username != "" { - return s.listUserSpecificNotificationsForUser(ctx, req.Filters.Username, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize) + return s.listUserSpecificNotificationsForUser(ctx, req) } return nil, trace.BadParameter("Invalid filters were provided, exactly one of GlobalOnly or Username must be defined.") @@ -168,6 +181,10 @@ func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.Li return nil, false } + if !labelsMatch(n.GetMetadata().GetLabels()) { + return nil, false + } + if !userNotifMatchFn(n) { return nil, false } @@ -184,6 +201,10 @@ func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.Li return nil, false } + if !labelsMatch(gn.GetMetadata().GetLabels()) { + return nil, false + } + if !s.matchGlobalNotification(ctx, authCtx, gn, notificationStatesMap) { return nil, false } @@ -583,8 +604,8 @@ func (s *Service) DeleteUserNotification(ctx context.Context, req *notifications } // 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 == "" { +func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) { + if req.GetFilters().GetUsername() == "" { return nil, trace.BadParameter("missing username") } @@ -602,15 +623,21 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user } stream := stream.FilterMap( - s.userNotificationCache.StreamUserNotifications(ctx, username, pageToken), + s.userNotificationCache.StreamUserNotifications(ctx, req.GetFilters().GetUsername(), req.GetPageToken()), func(n *notificationsv1.Notification) (*notificationsv1.Notification, bool) { - // If only user-created notifications are requested, filter by the user-creatd subkinds. - if userCreatedOnly && + // If only user-created notifications are requested, filter by the user-created subkinds. + if req.GetFilters().GetUserCreatedOnly() && n.GetSubKind() != types.NotificationUserCreatedInformationalSubKind && n.GetSubKind() != types.NotificationUserCreatedWarningSubKind { return nil, false } + for k, v := range req.GetFilters().GetLabels() { + if n.GetMetadata().GetLabels()[k] != v { + return nil, false + } + } + return n, true }) @@ -621,7 +648,7 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user item := stream.Item() if item != nil { notifications = append(notifications, item) - if len(notifications) == int(pageSize) { + if len(notifications) == int(req.GetPageSize()) { nextKey = item.GetMetadata().GetName() break } @@ -635,7 +662,7 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user } // 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) { +func (s *Service) listGlobalNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) { authCtx, err := s.authorizer.Authorize(ctx) if err != nil { return nil, trace.Wrap(err) @@ -646,15 +673,24 @@ func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly b } stream := stream.FilterMap( - s.globalNotificationCache.StreamGlobalNotifications(ctx, pageToken), + s.globalNotificationCache.StreamGlobalNotifications(ctx, req.GetPageToken()), func(gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, bool) { // If only user-created notifications are requested, filter by the user-creatd subkinds. - if userCreatedOnly && + if req.GetFilters().GetUserCreatedOnly() && gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedInformationalSubKind && gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedWarningSubKind { return nil, false } + // Pay special attention to the fact that we match against labels on the + // inner-notification resource spec, not the labels in the GlobalNotification's + // resource metadata. + for k, v := range req.GetFilters().GetLabels() { + if gn.GetSpec().GetNotification().GetMetadata().GetLabels()[k] != v { + return nil, false + } + } + return gn, true }) @@ -669,7 +705,7 @@ func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly b notifications = append(notifications, notification) - if len(notifications) == int(pageSize) { + if len(notifications) == int(req.GetPageSize()) { nextKey = item.GetMetadata().GetName() break } diff --git a/tool/tctl/common/notification_command.go b/tool/tctl/common/notification_command.go index 640b2e8b32b1c..860f8a9cf977c 100644 --- a/tool/tctl/common/notification_command.go +++ b/tool/tctl/common/notification_command.go @@ -29,6 +29,7 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" "github.com/gravitational/trace/trail" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/defaults" @@ -38,8 +39,10 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" + libclient "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/service/servicecfg" "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/tool/common" ) // NotificationCommand implements the `tctl notifications` family of commands. @@ -58,6 +61,8 @@ type NotificationCommand struct { title string content string + labels string + ttl time.Duration // stdout allows to switch the standard output source. Used in tests. stdout io.Writer @@ -74,11 +79,14 @@ func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *servicecfg 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.create.Flag("ttl", "Time duration after which the notification expires (default 30 days).").Default("30d").DurationVar(&n.ttl) + n.create.Flag("labels", "List of labels to attach to the notification. For example: key1=value1,key2=value2.").StringVar(&n.labels) 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.ls.Flag("labels", labelHelp).StringVar(&n.labels) 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) @@ -108,11 +116,17 @@ func (n *NotificationCommand) TryRun(ctx context.Context, cmd string, client *au // Create creates a new notification. func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Client) error { + labels, err := libclient.ParseLabelSpec(n.labels) + if err != nil { + return trace.Wrap(err) + } + + labels[types.NotificationTitleLabel] = n.title + labels[types.NotificationTextContentLabel] = n.content + meta := &headerv1.Metadata{ - Labels: map[string]string{ - types.NotificationTitleLabel: n.title, - types.NotificationTextContentLabel: n.content, - }, + Expires: timestamppb.New(time.Now().Add(n.ttl)), + Labels: labels, } subKind := types.NotificationUserCreatedInformationalSubKind @@ -219,6 +233,11 @@ func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Cli } func (n *NotificationCommand) List(ctx context.Context, client notificationspb.NotificationServiceClient) error { + labels, err := libclient.ParseLabelSpec(n.labels) + if err != nil { + return trace.Wrap(err) + } + var result []*notificationspb.Notification var pageToken string for { @@ -233,6 +252,7 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N Filters: ¬ificationspb.NotificationFilters{ Username: n.user, UserCreatedOnly: !n.allNotifications, + Labels: labels, }, }) if err != nil { @@ -245,6 +265,7 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N Filters: ¬ificationspb.NotificationFilters{ GlobalOnly: true, UserCreatedOnly: !n.allNotifications, + Labels: labels, }, }) if err != nil { @@ -266,13 +287,14 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N func displayNotifications(format string, notifications []*notificationspb.Notification, w io.Writer) { switch format { case teleport.Text: - table := asciitable.MakeTable([]string{"ID", "Created", "Expires", "Title"}) + table := asciitable.MakeTable([]string{"ID", "Created", "Expires", "Title", "Labels"}) 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], + common.FormatLabels(n.GetMetadata().GetLabels(), false), }) } fmt.Fprint(w, table.AsBuffer().String()) diff --git a/tool/tctl/common/notification_command_test.go b/tool/tctl/common/notification_command_test.go index 9309e45bfd727..982ea688310a4 100644 --- a/tool/tctl/common/notification_command_test.go +++ b/tool/tctl/common/notification_command_test.go @@ -64,7 +64,11 @@ func TestNotificationCommmandCRUD(t *testing.T) { 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."}) + buf, err = runNotificationsCommand(t, clt, []string{ + "create", "--roles", "test-1", "--title", "test-1 notification", + "--labels", "forrole=test-1", + "--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] @@ -90,6 +94,18 @@ func TestNotificationCommmandCRUD(t *testing.T) { assert.NotContains(collectT, buf.String(), "auditor notification") assert.NotContains(collectT, buf.String(), "manager notification") + // Filter out notifications with a non-existent label and make sure nothing comes back. + buf, err = runNotificationsCommand(t, clt, []string{"ls", "--labels=thislabel=doesnotexist"}) + assert.NotContains(collectT, buf.String(), "test-1 notification") + assert.NotContains(collectT, buf.String(), "auditor notification") + assert.NotContains(collectT, buf.String(), "manager notification") + + // Filter out global notifications with a valid label. + buf, err = runNotificationsCommand(t, clt, []string{"ls", "--labels=forrole=test-1"}) + 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.