implement listing notifications for a user with matching (#39942)

This commit is contained in:
Yassine Bounekhla
2024-05-15 19:26:58 +00:00
committed by GitHub
parent 265b58cb7e
commit f2c8833cea
21 changed files with 1448 additions and 1163 deletions
+27
View File
@@ -76,6 +76,7 @@ import (
kubewaitingcontainerpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/kubewaitingcontainer/v1"
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
notificationsv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
oktapb "github.com/gravitational/teleport/api/gen/proto/go/teleport/okta/v1"
pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1"
presencepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1"
@@ -111,6 +112,7 @@ type AuthServiceClient struct {
assist.AssistServiceClient
auditlogpb.AuditLogServiceClient
userpreferencespb.UserPreferencesServiceClient
notificationsv1pb.NotificationServiceClient
}
// Client is a gRPC Client that connects to a Teleport Auth server either
@@ -522,6 +524,7 @@ func (c *Client) dialGRPC(ctx context.Context, addr string) error {
AssistServiceClient: assist.NewAssistServiceClient(c.conn),
AuditLogServiceClient: auditlogpb.NewAuditLogServiceClient(c.conn),
UserPreferencesServiceClient: userpreferencespb.NewUserPreferencesServiceClient(c.conn),
NotificationServiceClient: notificationsv1pb.NewNotificationServiceClient(c.conn),
}
c.JoinServiceClient = NewJoinServiceClient(proto.NewJoinServiceClient(c.conn))
@@ -865,6 +868,11 @@ func (c *Client) WorkloadIdentityServiceClient() machineidv1pb.WorkloadIdentityS
return machineidv1pb.NewWorkloadIdentityServiceClient(c.conn)
}
// NotificationServiceClient returns a notification service client that can be used to fetch notifications.
func (c *Client) NotificationServiceClient() notificationsv1pb.NotificationServiceClient {
return notificationsv1pb.NewNotificationServiceClient(c.conn)
}
// Ping gets basic info about the auth server.
func (c *Client) Ping(ctx context.Context) (proto.PingResponse, error) {
rsp, err := c.grpc.Ping(ctx, &proto.PingRequest{})
@@ -4899,6 +4907,25 @@ func (c *Client) UpsertUserPreferences(ctx context.Context, in *userpreferencesp
return nil
}
// ListNotifications returns a paginated list of notifications for the user.
// This includes global notifications which match the user, as well as user-specific notifications for the user.
func (c *Client) ListNotifications(ctx context.Context, req *notificationsv1pb.ListNotificationsRequest) (*notificationsv1pb.ListNotificationsResponse, error) {
rsp, err := c.NotificationServiceClient().ListNotifications(ctx, req)
return rsp, 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)
return rsp, trace.Wrap(err)
}
// UpsertUserLastSeenNotification creates or updates a user's last seen notification timestamp.
func (c *Client) UpsertUserLastSeenNotification(ctx context.Context, req *notificationsv1pb.UpsertUserLastSeenNotificationRequest) (*notificationsv1pb.UserLastSeenNotification, error) {
rsp, err := c.NotificationServiceClient().UpsertUserLastSeenNotification(ctx, req)
return rsp, trace.Wrap(err)
}
// ResourceUsageClient returns an unadorned Resource Usage service client,
// using the underlying Auth gRPC connection.
// Clients connecting to non-Enterprise clusters, or older Teleport versions,
@@ -183,8 +183,6 @@ type NotificationSpec struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// id is the UUID of the notification, in UUIDv7 format.
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// created is when the notification was created, in UNIX time.
Created *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created,proto3" json:"created,omitempty"`
// unscoped is whether the notification shouldn't be restricted to a specific audience. This is to prevent the potential future possibility that a user-specific notification contains information that the user should no longer be allowed to see. Default is true.
@@ -225,13 +223,6 @@ func (*NotificationSpec) Descriptor() ([]byte, []int) {
return file_teleport_notifications_v1_notifications_proto_rawDescGZIP(), []int{1}
}
func (x *NotificationSpec) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *NotificationSpec) GetCreated() *timestamppb.Timestamp {
if x != nil {
return x.Created
@@ -960,133 +951,132 @@ var file_teleport_notifications_v1_notifications_proto_rawDesc = []byte{
0x01, 0x28, 0x0b, 0x32, 0x2b, 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, 0x53, 0x70, 0x65, 0x63,
0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x90, 0x01, 0x0a, 0x10, 0x4e, 0x6f, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x12, 0x0e, 0x0a, 0x02, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x63,
0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0x8a, 0x01, 0x0a, 0x10, 0x4e, 0x6f, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x12, 0x34, 0x0a, 0x07, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x02, 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, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x6e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x08, 0x52, 0x08, 0x75, 0x6e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x64, 0x12, 0x1a, 0x0a,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0xde, 0x01, 0x0a, 0x12, 0x47, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12,
0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65,
0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x12, 0x45, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x31, 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,
0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x22, 0xca, 0x02, 0x0a, 0x16, 0x47,
0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x53, 0x70, 0x65, 0x63, 0x12, 0x51, 0x0a, 0x0e, 0x62, 0x79, 0x5f, 0x70, 0x65, 0x72, 0x6d,
0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 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, 0x42, 0x79, 0x50, 0x65, 0x72, 0x6d,
0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x48, 0x00, 0x52, 0x0d, 0x62, 0x79, 0x50, 0x65, 0x72,
0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x08, 0x62, 0x79, 0x5f, 0x72,
0x6f, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 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, 0x42, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x48, 0x00,
0x52, 0x07, 0x62, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x03, 0x61, 0x6c, 0x6c,
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x30, 0x0a,
0x14, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x6d, 0x61, 0x74,
0x63, 0x68, 0x41, 0x6c, 0x6c, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12,
0x4b, 0x0a, 0x0c, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x05, 0x20, 0x01, 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, 0x0c,
0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07,
0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x22, 0x4f, 0x0a, 0x0d, 0x42, 0x79, 0x50, 0x65, 0x72,
0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x72, 0x6f, 0x6c, 0x65,
0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x15, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x43, 0x6f,
0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x43, 0x6f,
0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x1f, 0x0a, 0x07, 0x42, 0x79, 0x52, 0x6f,
0x6c, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x09, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x22, 0xb4, 0x02, 0x0a, 0x15, 0x55, 0x73,
0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74,
0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b,
0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69,
0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08,
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c,
0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x48, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e,
0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x52,
0x02, 0x69, 0x64, 0x22, 0xde, 0x01, 0x0a, 0x12, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e, 0x6f,
0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69,
0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19,
0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18,
0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74,
0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x45, 0x0a,
0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 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, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04,
0x73, 0x70, 0x65, 0x63, 0x22, 0xca, 0x02, 0x0a, 0x16, 0x47, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x4e,
0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x12,
0x51, 0x0a, 0x0e, 0x62, 0x79, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 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, 0x42, 0x79, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e,
0x73, 0x48, 0x00, 0x52, 0x0d, 0x62, 0x79, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x12, 0x3f, 0x0a, 0x08, 0x62, 0x79, 0x5f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x02,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 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, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63,
0x12, 0x4e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x36, 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, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x22, 0x44, 0x0a, 0x19, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, 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, 0x7a, 0x0a, 0x1b, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f,
0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x5b, 0x0a, 0x12, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0e, 0x32, 0x2c, 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, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52,
0x11, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61,
0x74, 0x65, 0x22, 0xc9, 0x02, 0x0a, 0x18, 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,
0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b,
0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18,
0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c,
0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e,
0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x12, 0x4b, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x37, 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, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12,
0x51, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x39, 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,
0x2e, 0x42, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x48, 0x00, 0x52, 0x07, 0x62, 0x79, 0x52, 0x6f,
0x6c, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x48, 0x00, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x12, 0x30, 0x0a, 0x14, 0x6d, 0x61, 0x74, 0x63, 0x68,
0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x41, 0x6c, 0x6c, 0x43,
0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x4b, 0x0a, 0x0c, 0x6e, 0x6f, 0x74,
0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 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, 0x0c, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69,
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65,
0x72, 0x22, 0x4f, 0x0a, 0x0d, 0x42, 0x79, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f,
0x6e, 0x73, 0x12, 0x3e, 0x0a, 0x0f, 0x72, 0x6f, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x64, 0x69,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x52, 0x0e, 0x72, 0x6f, 0x6c, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x22, 0x1f, 0x0a, 0x07, 0x42, 0x79, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x14, 0x0a,
0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x72, 0x6f,
0x6c, 0x65, 0x73, 0x22, 0xb4, 0x02, 0x0a, 0x15, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e,
0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70,
0x6f, 0x72, 0x74, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
0x12, 0x48, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 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, 0x55, 0x73, 0x65, 0x72, 0x4e,
0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65,
0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x4e, 0x0a, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x36, 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, 0x53, 0x74, 0x61, 0x74,
0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x44, 0x0a, 0x19, 0x55, 0x73,
0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74,
0x61, 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, 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, 0x7a, 0x0a, 0x1b, 0x55, 0x73, 0x65, 0x72, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
0x5b, 0x0a, 0x12, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, 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, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x11, 0x6e, 0x6f, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc9, 0x02, 0x0a,
0x18, 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, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a,
0x08, 0x73, 0x75, 0x62, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x07, 0x73, 0x75, 0x62, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x4b, 0x0a, 0x04,
0x73, 0x70, 0x65, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 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, 0x53,
0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x51, 0x0a, 0x06, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 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, 0x53, 0x74,
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4a, 0x04, 0x08, 0x06,
0x10, 0x07, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x1e, 0x0a, 0x1c, 0x55, 0x73, 0x65, 0x72,
0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x22, 0x1e,
0x0a, 0x1c, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f,
0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x22, 0x62,
0x0a, 0x1e, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f,
0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x12, 0x40, 0x0a, 0x0e, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x69,
0x6d, 0x65, 0x18, 0x01, 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, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54, 0x69,
0x6d, 0x65, 0x2a, 0x79, 0x0a, 0x11, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x22, 0x0a, 0x1e, 0x4e, 0x4f, 0x54, 0x49, 0x46,
0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e,
0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4e,
0x4f, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54,
0x45, 0x5f, 0x43, 0x4c, 0x49, 0x43, 0x4b, 0x45, 0x44, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x4e,
0x4f, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54,
0x45, 0x5f, 0x44, 0x49, 0x53, 0x4d, 0x49, 0x53, 0x53, 0x45, 0x44, 0x10, 0x02, 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, 0x6f, 0x6e, 0x53, 0x70, 0x65, 0x63, 0x22, 0x62, 0x0a, 0x1e, 0x55, 0x73, 0x65, 0x72,
0x4c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x40, 0x0a, 0x0e, 0x6c, 0x61,
0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 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, 0x0c,
0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x2a, 0x79, 0x0a, 0x11,
0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74,
0x65, 0x12, 0x22, 0x0a, 0x1e, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x49, 0x4f,
0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46,
0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x49, 0x43,
0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x43, 0x4c, 0x49, 0x43,
0x4b, 0x45, 0x44, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x49, 0x43,
0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x49, 0x53, 0x4d,
0x49, 0x53, 0x53, 0x45, 0x44, 0x10, 0x02, 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 (
File diff suppressed because it is too large Load Diff
@@ -41,13 +41,9 @@ const (
NotificationService_DeleteUserNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/DeleteUserNotification"
NotificationService_CreateGlobalNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/CreateGlobalNotification"
NotificationService_DeleteGlobalNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/DeleteGlobalNotification"
NotificationService_ListUserNotifications_FullMethodName = "/teleport.notifications.v1.NotificationService/ListUserNotifications"
NotificationService_ListNotifications_FullMethodName = "/teleport.notifications.v1.NotificationService/ListNotifications"
NotificationService_UpsertUserNotificationState_FullMethodName = "/teleport.notifications.v1.NotificationService/UpsertUserNotificationState"
NotificationService_DeleteUserNotificationState_FullMethodName = "/teleport.notifications.v1.NotificationService/DeleteUserNotificationState"
NotificationService_ListUserNotificationStates_FullMethodName = "/teleport.notifications.v1.NotificationService/ListUserNotificationStates"
NotificationService_GetUserLastSeenNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/GetUserLastSeenNotification"
NotificationService_UpsertUserLastSeenNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/UpsertUserLastSeenNotification"
NotificationService_DeleteUserLastSeenNotification_FullMethodName = "/teleport.notifications.v1.NotificationService/DeleteUserLastSeenNotification"
)
// NotificationServiceClient is the client API for NotificationService service.
@@ -62,20 +58,12 @@ type NotificationServiceClient interface {
CreateGlobalNotification(ctx context.Context, in *CreateGlobalNotificationRequest, opts ...grpc.CallOption) (*GlobalNotification, error)
// DeleteGlobalNotification deletes a global notification.
DeleteGlobalNotification(ctx context.Context, in *DeleteGlobalNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListUserNotificationsRequest is the request for listing a user's notifications, which include user-specific ones as well as global notifications that match them.
ListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error)
// ListNotifications returns a paginated list of a user's notifications.
ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error)
// UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification.
UpsertUserNotificationState(ctx context.Context, in *UpsertUserNotificationStateRequest, opts ...grpc.CallOption) (*UserNotificationState, error)
// DeleteUserNotificationState deletes a user notification state object.
DeleteUserNotificationState(ctx context.Context, in *DeleteUserNotificationStateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListUserNotificationStates returns a page of a user's notification states.
ListUserNotificationStates(ctx context.Context, in *ListUserNotificationStatesRequest, opts ...grpc.CallOption) (*ListUserNotificationStatesResponse, error)
// GetUserLastSeenNotification returns a user's last seen notification item.
GetUserLastSeenNotification(ctx context.Context, in *GetUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*UserLastSeenNotification, error)
// UpsertUserLastSeenNotification creates or updates a user's last seen notification item.
UpsertUserLastSeenNotification(ctx context.Context, in *UpsertUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*UserLastSeenNotification, error)
// DeleteUserLastSeenNotification deletes a user's last seen notification item.
DeleteUserLastSeenNotification(ctx context.Context, in *DeleteUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type notificationServiceClient struct {
@@ -122,9 +110,9 @@ func (c *notificationServiceClient) DeleteGlobalNotification(ctx context.Context
return out, nil
}
func (c *notificationServiceClient) ListUserNotifications(ctx context.Context, in *ListUserNotificationsRequest, opts ...grpc.CallOption) (*ListUserNotificationsResponse, error) {
out := new(ListUserNotificationsResponse)
err := c.cc.Invoke(ctx, NotificationService_ListUserNotifications_FullMethodName, in, out, opts...)
func (c *notificationServiceClient) ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error) {
out := new(ListNotificationsResponse)
err := c.cc.Invoke(ctx, NotificationService_ListNotifications_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
@@ -140,33 +128,6 @@ func (c *notificationServiceClient) UpsertUserNotificationState(ctx context.Cont
return out, nil
}
func (c *notificationServiceClient) DeleteUserNotificationState(ctx context.Context, in *DeleteUserNotificationStateRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, NotificationService_DeleteUserNotificationState_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationServiceClient) ListUserNotificationStates(ctx context.Context, in *ListUserNotificationStatesRequest, opts ...grpc.CallOption) (*ListUserNotificationStatesResponse, error) {
out := new(ListUserNotificationStatesResponse)
err := c.cc.Invoke(ctx, NotificationService_ListUserNotificationStates_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationServiceClient) GetUserLastSeenNotification(ctx context.Context, in *GetUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*UserLastSeenNotification, error) {
out := new(UserLastSeenNotification)
err := c.cc.Invoke(ctx, NotificationService_GetUserLastSeenNotification_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notificationServiceClient) UpsertUserLastSeenNotification(ctx context.Context, in *UpsertUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*UserLastSeenNotification, error) {
out := new(UserLastSeenNotification)
err := c.cc.Invoke(ctx, NotificationService_UpsertUserLastSeenNotification_FullMethodName, in, out, opts...)
@@ -176,15 +137,6 @@ func (c *notificationServiceClient) UpsertUserLastSeenNotification(ctx context.C
return out, nil
}
func (c *notificationServiceClient) DeleteUserLastSeenNotification(ctx context.Context, in *DeleteUserLastSeenNotificationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, NotificationService_DeleteUserLastSeenNotification_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// NotificationServiceServer is the server API for NotificationService service.
// All implementations must embed UnimplementedNotificationServiceServer
// for forward compatibility
@@ -197,20 +149,12 @@ type NotificationServiceServer interface {
CreateGlobalNotification(context.Context, *CreateGlobalNotificationRequest) (*GlobalNotification, error)
// DeleteGlobalNotification deletes a global notification.
DeleteGlobalNotification(context.Context, *DeleteGlobalNotificationRequest) (*emptypb.Empty, error)
// ListUserNotificationsRequest is the request for listing a user's notifications, which include user-specific ones as well as global notifications that match them.
ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error)
// ListNotifications returns a paginated list of a user's notifications.
ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error)
// UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification.
UpsertUserNotificationState(context.Context, *UpsertUserNotificationStateRequest) (*UserNotificationState, error)
// DeleteUserNotificationState deletes a user notification state object.
DeleteUserNotificationState(context.Context, *DeleteUserNotificationStateRequest) (*emptypb.Empty, error)
// ListUserNotificationStates returns a page of a user's notification states.
ListUserNotificationStates(context.Context, *ListUserNotificationStatesRequest) (*ListUserNotificationStatesResponse, error)
// GetUserLastSeenNotification returns a user's last seen notification item.
GetUserLastSeenNotification(context.Context, *GetUserLastSeenNotificationRequest) (*UserLastSeenNotification, error)
// UpsertUserLastSeenNotification creates or updates a user's last seen notification item.
UpsertUserLastSeenNotification(context.Context, *UpsertUserLastSeenNotificationRequest) (*UserLastSeenNotification, error)
// DeleteUserLastSeenNotification deletes a user's last seen notification item.
DeleteUserLastSeenNotification(context.Context, *DeleteUserLastSeenNotificationRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedNotificationServiceServer()
}
@@ -230,27 +174,15 @@ func (UnimplementedNotificationServiceServer) CreateGlobalNotification(context.C
func (UnimplementedNotificationServiceServer) DeleteGlobalNotification(context.Context, *DeleteGlobalNotificationRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteGlobalNotification not implemented")
}
func (UnimplementedNotificationServiceServer) ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserNotifications not implemented")
func (UnimplementedNotificationServiceServer) ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListNotifications not implemented")
}
func (UnimplementedNotificationServiceServer) UpsertUserNotificationState(context.Context, *UpsertUserNotificationStateRequest) (*UserNotificationState, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpsertUserNotificationState not implemented")
}
func (UnimplementedNotificationServiceServer) DeleteUserNotificationState(context.Context, *DeleteUserNotificationStateRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserNotificationState not implemented")
}
func (UnimplementedNotificationServiceServer) ListUserNotificationStates(context.Context, *ListUserNotificationStatesRequest) (*ListUserNotificationStatesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListUserNotificationStates not implemented")
}
func (UnimplementedNotificationServiceServer) GetUserLastSeenNotification(context.Context, *GetUserLastSeenNotificationRequest) (*UserLastSeenNotification, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserLastSeenNotification not implemented")
}
func (UnimplementedNotificationServiceServer) UpsertUserLastSeenNotification(context.Context, *UpsertUserLastSeenNotificationRequest) (*UserLastSeenNotification, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpsertUserLastSeenNotification not implemented")
}
func (UnimplementedNotificationServiceServer) DeleteUserLastSeenNotification(context.Context, *DeleteUserLastSeenNotificationRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserLastSeenNotification not implemented")
}
func (UnimplementedNotificationServiceServer) mustEmbedUnimplementedNotificationServiceServer() {}
// UnsafeNotificationServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -336,20 +268,20 @@ func _NotificationService_DeleteGlobalNotification_Handler(srv interface{}, ctx
return interceptor(ctx, in, info, handler)
}
func _NotificationService_ListUserNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserNotificationsRequest)
func _NotificationService_ListNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListNotificationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationServiceServer).ListUserNotifications(ctx, in)
return srv.(NotificationServiceServer).ListNotifications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationService_ListUserNotifications_FullMethodName,
FullMethod: NotificationService_ListNotifications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationServiceServer).ListUserNotifications(ctx, req.(*ListUserNotificationsRequest))
return srv.(NotificationServiceServer).ListNotifications(ctx, req.(*ListNotificationsRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -372,60 +304,6 @@ func _NotificationService_UpsertUserNotificationState_Handler(srv interface{}, c
return interceptor(ctx, in, info, handler)
}
func _NotificationService_DeleteUserNotificationState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteUserNotificationStateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationServiceServer).DeleteUserNotificationState(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationService_DeleteUserNotificationState_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationServiceServer).DeleteUserNotificationState(ctx, req.(*DeleteUserNotificationStateRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationService_ListUserNotificationStates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserNotificationStatesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationServiceServer).ListUserNotificationStates(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationService_ListUserNotificationStates_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationServiceServer).ListUserNotificationStates(ctx, req.(*ListUserNotificationStatesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationService_GetUserLastSeenNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserLastSeenNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationServiceServer).GetUserLastSeenNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationService_GetUserLastSeenNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationServiceServer).GetUserLastSeenNotification(ctx, req.(*GetUserLastSeenNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotificationService_UpsertUserLastSeenNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpsertUserLastSeenNotificationRequest)
if err := dec(in); err != nil {
@@ -444,24 +322,6 @@ func _NotificationService_UpsertUserLastSeenNotification_Handler(srv interface{}
return interceptor(ctx, in, info, handler)
}
func _NotificationService_DeleteUserLastSeenNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteUserLastSeenNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotificationServiceServer).DeleteUserLastSeenNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotificationService_DeleteUserLastSeenNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotificationServiceServer).DeleteUserLastSeenNotification(ctx, req.(*DeleteUserLastSeenNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
// NotificationService_ServiceDesc is the grpc.ServiceDesc for NotificationService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -486,33 +346,17 @@ var NotificationService_ServiceDesc = grpc.ServiceDesc{
Handler: _NotificationService_DeleteGlobalNotification_Handler,
},
{
MethodName: "ListUserNotifications",
Handler: _NotificationService_ListUserNotifications_Handler,
MethodName: "ListNotifications",
Handler: _NotificationService_ListNotifications_Handler,
},
{
MethodName: "UpsertUserNotificationState",
Handler: _NotificationService_UpsertUserNotificationState_Handler,
},
{
MethodName: "DeleteUserNotificationState",
Handler: _NotificationService_DeleteUserNotificationState_Handler,
},
{
MethodName: "ListUserNotificationStates",
Handler: _NotificationService_ListUserNotificationStates_Handler,
},
{
MethodName: "GetUserLastSeenNotification",
Handler: _NotificationService_GetUserLastSeenNotification_Handler,
},
{
MethodName: "UpsertUserLastSeenNotification",
Handler: _NotificationService_UpsertUserLastSeenNotification_Handler,
},
{
MethodName: "DeleteUserLastSeenNotification",
Handler: _NotificationService_DeleteUserLastSeenNotification_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "teleport/notifications/v1/notifications_service.proto",
@@ -42,8 +42,8 @@ message Notification {
// NotificationSpec is the notification specification.
message NotificationSpec {
// id is the UUID of the notification, in UUIDv7 format.
string id = 1;
reserved 1;
reserved "id";
// created is when the notification was created, in UNIX time.
google.protobuf.Timestamp created = 2;
// unscoped is whether the notification shouldn't be restricted to a specific audience. This is to prevent the potential future possibility that a user-specific notification contains information that the user should no longer be allowed to see. Default is true.
@@ -21,6 +21,7 @@ syntax = "proto3";
package teleport.notifications.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "teleport/notifications/v1/notifications.proto";
option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1;notificationsv1";
@@ -37,22 +38,14 @@ service NotificationService {
// DeleteGlobalNotification deletes a global notification.
rpc DeleteGlobalNotification(DeleteGlobalNotificationRequest) returns (google.protobuf.Empty);
// ListUserNotificationsRequest is the request for listing a user's notifications, which include user-specific ones as well as global notifications that match them.
rpc ListUserNotifications(ListUserNotificationsRequest) returns (ListUserNotificationsResponse);
// ListNotifications returns a paginated list of a user's notifications.
rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse);
// UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification.
rpc UpsertUserNotificationState(UpsertUserNotificationStateRequest) returns (UserNotificationState);
// DeleteUserNotificationState deletes a user notification state object.
rpc DeleteUserNotificationState(DeleteUserNotificationStateRequest) returns (google.protobuf.Empty);
// ListUserNotificationStates returns a page of a user's notification states.
rpc ListUserNotificationStates(ListUserNotificationStatesRequest) returns (ListUserNotificationStatesResponse);
// GetUserLastSeenNotification returns a user's last seen notification item.
rpc GetUserLastSeenNotification(GetUserLastSeenNotificationRequest) returns (UserLastSeenNotification);
// UpsertUserLastSeenNotification creates or updates a user's last seen notification item.
rpc UpsertUserLastSeenNotification(UpsertUserLastSeenNotificationRequest) returns (UserLastSeenNotification);
// DeleteUserLastSeenNotification deletes a user's last seen notification item.
rpc DeleteUserLastSeenNotification(DeleteUserLastSeenNotificationRequest) returns (google.protobuf.Empty);
}
// CreateUserNotificationRequest is the request for creating a user-specific notification.
@@ -71,22 +64,22 @@ message DeleteUserNotificationRequest {
string notification_id = 2;
}
// ListUserNotificationsRequest is the request for listing a user's notifications, which include user-specific ones as well as global notifications that match them.
message ListUserNotificationsRequest {
// username is the username of the user the notifications to list are for.
string username = 1;
// ListNotificationsRequest is the request for listing a user's notifications.
message ListNotificationsRequest {
// page_size is the size of the page to return.
int32 page_size = 2;
int32 page_size = 1;
// page_token is the next_page_token value returned from a previous ListUserNotifications request, if any.
string page_token = 3;
string page_token = 2;
}
// ListUserNotificationsResponse is the response from listing a user's user-specific notifications.
message ListUserNotificationsResponse {
// ListNotificationsResponse is the response from listing a user's notifications.
message ListNotificationsResponse {
// notifications is the notifications returned.
repeated Notification notifications = 1;
// next_page_token is the token to retrieve the next page of results, this will be empty if there are no more results.
string next_page_token = 2;
// next_page_token contains the next page token to use as the start key for the next page of notifications.
string next_page_token = 3;
// user_last_seen_notification_timestamp is the timestamp of the last notification the user has seen.
google.protobuf.Timestamp user_last_seen_notification_timestamp = 4;
}
// CreateGlobalNotificationRequest is the request for creating a global notification.
@@ -109,52 +102,10 @@ message UpsertUserNotificationStateRequest {
UserNotificationState user_notification_state = 2;
}
// UpdateUserNotificationStateRequest is the request for updating a user notification state.
message UpdateUserNotificationStateRequest {
// username is the username of the user.
string username = 1;
// user_notification_state is the updated user notification state.
UserNotificationState user_notification_state = 2;
}
// DeleteUserNotificationState is the request for deleting a user notification state.
message DeleteUserNotificationStateRequest {
// username is the username of the user.
string username = 1;
// notification_id is the ID of the notification the notifiation state to delete is for.
string notification_id = 2;
}
// ListUserNotificationStatesRequest is the request for listing a user's notification states.
message ListUserNotificationStatesRequest {
// username is the username of the user.
string username = 1;
}
// ListUserNotificationStatesResponse is the response from listing a user's notification states.
message ListUserNotificationStatesResponse {
// notification_states is the user notification state items returned.
repeated UserNotificationState notification_states = 1;
// next_page_token is the token to retrieve the next page of results, this will be empty if there are no more results.
string next_page_token = 2;
}
// GetUserLastSeenNotificationRequest is the request for getting a user's last seen notification.
message GetUserLastSeenNotificationRequest {
// username is the username of the user.
string username = 1;
}
// UpsertUserLastSeenNotificationRequest is the request for creating or updating a user's last seen notification.
message UpsertUserLastSeenNotificationRequest {
// username is the username of the user.
string username = 1;
// user_notification_state is the udpated user last seen notification item.
// user_notification_state is the updated user last seen notification item.
UserLastSeenNotification user_last_seen_notification = 2;
}
// DeleteUserLastSeenNotificationRequest is the request for deleting a user's last seen notification.
message DeleteUserLastSeenNotificationRequest {
// username is the username of the user.
string username = 1;
}
+9
View File
@@ -986,6 +986,15 @@ const (
PluginGenerationLabel = TeleportInternalLabelPrefix + "plugin-generation"
)
const (
// NotificationTitleLabel is the label which contains the title of the notification.
NotificationTitleLabel = TeleportInternalLabelPrefix + "title"
// NotificationClickedLabel is the label which contains whether the notification has been clicked on by the user.
NotificationClickedLabel = TeleportInternalLabelPrefix + "clicked"
// NotificationScope is the label which contains the scope of the notification, either "user" or "global"
NotificationScope = TeleportInternalLabelPrefix + "scope"
)
const (
// InstallMethodAWSOIDCDeployServiceEnvVar is the env var used to detect if the agent was installed
// using the DeployService action of the AWS OIDC integration.
+26 -12
View File
@@ -629,6 +629,10 @@ func (c *Client) IntegrationAWSOIDCClient() integrationv1.AWSOIDCServiceClient {
return integrationv1.NewAWSOIDCServiceClient(c.APIClient.GetConnection())
}
func (c *Client) NotificationServiceClient() notificationsv1.NotificationServiceClient {
return notificationsv1.NewNotificationServiceClient(c.APIClient.GetConnection())
}
// DiscoveryConfigClient returns a client for managing the DiscoveryConfig resource.
func (c *Client) DiscoveryConfigClient() services.DiscoveryConfigWithStatusUpdater {
return c.APIClient.DiscoveryConfigClient()
@@ -654,6 +658,22 @@ func (c *Client) SetStaticTokens(st types.StaticTokens) error {
return trace.NotImplemented(notImplementedMessage)
}
// 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, username string, uns *notificationsv1.UserNotificationState) (*notificationsv1.UserNotificationState, error) {
return c.APIClient.UpsertUserNotificationState(ctx, &notificationsv1.UpsertUserNotificationStateRequest{
Username: username,
UserNotificationState: uns,
})
}
// UpsertUserLastSeenNotification creates or updates a user's last seen notification item.
func (c *Client) UpsertUserLastSeenNotification(ctx context.Context, username string, ulsn *notificationsv1.UserLastSeenNotification) (*notificationsv1.UserLastSeenNotification, error) {
return c.APIClient.UpsertUserLastSeenNotification(ctx, &notificationsv1.UpsertUserLastSeenNotificationRequest{
Username: username,
UserLastSeenNotification: ulsn,
})
}
// 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
@@ -672,18 +692,6 @@ func (c *Client) DeleteGlobalNotification(ctx context.Context, notificationId st
return trace.NotImplemented(notImplementedMessage)
}
// 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, username string, state *notificationsv1.UserNotificationState) (*notificationsv1.UserNotificationState, error) {
// TODO(rudream): implement client methods for notifications
return nil, trace.NotImplemented(notImplementedMessage)
}
// UpsertUserLastSeenNotification creates or updates a user's last seen notification item.
func (c *Client) UpsertUserLastSeenNotification(ctx context.Context, username string, ulsn *notificationsv1.UserLastSeenNotification) (*notificationsv1.UserLastSeenNotification, error) {
// TODO(rudream): implement client methods for notifications
return nil, trace.NotImplemented(notImplementedMessage)
}
// DeleteAllGlobalNotifications not implemented: can only be called locally.
func (c *Client) DeleteAllGlobalNotifications(ctx context.Context) error {
return trace.NotImplemented(notImplementedMessage)
@@ -1608,6 +1616,12 @@ type ClientI interface {
// (as per the default gRPC behavior).
WorkloadIdentityServiceClient() machineidv1pb.WorkloadIdentityServiceClient
// NotificationServiceClient returns a notification service client.
// Clients connecting to older Teleport versions, still get a client
// when calling this method, but all RPCs will return "not implemented" errors
// (as per the default gRPC behavior).
NotificationServiceClient() notificationsv1.NotificationServiceClient
// ClusterConfigClient returns a Cluster Configuration client.
// Clients connecting to non-Enterprise clusters, or older Teleport versions,
// still get a client when calling this method, but all RPCs will return
+14
View File
@@ -60,6 +60,7 @@ import (
loginrulepb "github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1"
machineidv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
oktapb "github.com/gravitational/teleport/api/gen/proto/go/teleport/okta/v1"
presencev1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
@@ -83,6 +84,7 @@ import (
kubewaitingcontainerv1 "github.com/gravitational/teleport/lib/auth/kubewaitingcontainer"
"github.com/gravitational/teleport/lib/auth/loginrule"
"github.com/gravitational/teleport/lib/auth/machineid/machineidv1"
notifications "github.com/gravitational/teleport/lib/auth/notifications/notificationsv1"
"github.com/gravitational/teleport/lib/auth/okta"
"github.com/gravitational/teleport/lib/auth/presence/presencev1"
"github.com/gravitational/teleport/lib/auth/trust/trustv1"
@@ -5300,6 +5302,18 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) {
}
accessmonitoringrules.RegisterAccessMonitoringRulesServiceServer(server, accessMonitoringRuleServer)
// Initialize and register the notifications service.
notificationsServer, err := notifications.NewService(notifications.ServiceConfig{
Authorizer: cfg.Authorizer,
Backend: cfg.AuthServer.Services,
UserNotificationCache: cfg.AuthServer.UserNotificationCache,
GlobalNotificationCache: cfg.AuthServer.GlobalNotificationCache,
})
if err != nil {
return nil, trace.Wrap(err)
}
notificationsv1.RegisterNotificationServiceServer(server, notificationsServer)
// Only register the service if this is an open source build. Enterprise builds
// register the actual service via an auth plugin, if we register here then all
// Enterprise builds would fail with a duplicate service registered error.
+1
View File
@@ -298,6 +298,7 @@ func NewTestAuthServer(cfg TestAuthServerConfig) (*TestAuthServer, error) {
SkipPeriodicOperations: true,
Emitter: emitter,
TraceClient: cfg.TraceClient,
Clock: cfg.Clock,
KeyStoreConfig: keystore.Config{
Software: keystore.SoftwareConfig{
RSAKeyPairSource: authority.New().GenerateKeyPair,
+445
View File
@@ -0,0 +1,445 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package auth
import (
"context"
"fmt"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
)
func TestNotifications(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
fakeClock := clockwork.NewFakeClock()
authServer, err := NewTestAuthServer(TestAuthServerConfig{
Dir: t.TempDir(),
Clock: fakeClock,
CacheEnabled: true,
})
require.NoError(t, err)
srv, err := authServer.NewTestTLSServer()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, srv.Close()) })
roles := map[string]types.RoleSpecV6{
// auditors have access to nodes and databases, and the "auditor" and "user" logins
"auditors": {
Allow: types.RoleConditions{
Logins: []string{"auditor", "user"},
Rules: []types.Rule{
{
Resources: []string{types.KindNode, types.KindDatabase},
Verbs: services.RW(),
},
},
},
},
// managers have access to review requests for the "intern" role
"managers": {
Allow: types.RoleConditions{
Logins: []string{"user"},
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"intern"},
},
},
},
}
for roleName, roleSpec := range roles {
role, err := types.NewRole(roleName, roleSpec)
require.NoError(t, err)
_, err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
}
auditorUsername := "auditor"
managerUsername := "manager"
userRolesMap := map[string][]string{
auditorUsername: {"auditors"},
managerUsername: {"managers"},
}
// Create the users with their roles.
for username, roles := range userRolesMap {
user, err := types.NewUser(username)
require.NoError(t, err)
user.SetRoles(roles)
_, err = srv.Auth().UpsertUser(ctx, user)
require.NoError(t, err)
}
// Describes a set of mock notifications to be created.
// The number in each notification's description represents the creation order of the notifications.
// eg. "auditor-3" will be the third notification created for auditor, and the third from last in the list (since it's the third oldest),
testNotifications := []struct {
userNotification *notificationsv1.Notification
globalNotification *notificationsv1.GlobalNotification
}{
{
userNotification: newUserNotification(t, auditorUsername, "auditor-1"),
},
{
userNotification: newUserNotification(t, auditorUsername, "auditor-2"),
},
{
// Matcher matches by the role "auditors"
globalNotification: newGlobalNotification(t, "auditor-3", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByRoles{
ByRoles: &notificationsv1.ByRoles{
Roles: []string{userRolesMap[auditorUsername][0]},
},
},
}),
},
{
// Matcher matches by the role "managers"
globalNotification: newGlobalNotification(t, "manager-1", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByRoles{
ByRoles: &notificationsv1.ByRoles{
Roles: []string{userRolesMap[managerUsername][0]},
},
},
}),
},
{
userNotification: newUserNotification(t, auditorUsername, "auditor-4"),
},
{
// Matcher matches all, both users should see this.
globalNotification: newGlobalNotification(t, "auditor-5,manager-2", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_All{
All: true,
},
}),
},
{
userNotification: newUserNotification(t, managerUsername, "manager-3"),
},
{
// Matcher matches by read & write permission on nodes.
globalNotification: newGlobalNotification(t, "auditor-6", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
Rules: []types.Rule{
{
Resources: []string{types.KindNode},
Verbs: services.RW(),
},
},
},
},
},
},
}),
},
{
// Matcher matches by the logins "auditor" and "user". Auditor has both of them, but manager only has "user", since we set MatchAllConditions to true, only auditor
// should get this notification.
globalNotification: newGlobalNotification(t, "auditor-7", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
Logins: []string{"auditor"},
},
{
Logins: []string{"user"},
},
},
},
},
MatchAllConditions: true,
}),
},
{
// Matcher matches by permission to review access requests for "intern"
globalNotification: newGlobalNotification(t, "manager-4", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
ReviewRequests: &types.AccessReviewConditions{
Roles: []string{"intern"},
},
},
},
},
},
}),
},
{
userNotification: newUserNotification(t, managerUsername, "manager-5"),
},
{
// Matcher matches by the logins "auditor" and "user". Both of them have "user" and MatchAllConditions is false, so both should get this notification.
globalNotification: newGlobalNotification(t, "auditor-8,manager-6", &notificationsv1.GlobalNotificationSpec{
Matcher: &notificationsv1.GlobalNotificationSpec_ByPermissions{
ByPermissions: &notificationsv1.ByPermissions{
RoleConditions: []*types.RoleConditions{
{
Logins: []string{"auditor"},
},
{
Logins: []string{"user"},
},
},
},
},
}),
},
}
notificationIdMap := map[string]string{}
// Create the notifications.
for _, n := range testNotifications {
// We add a small delay to ensure that the timestamps in the generated UUID's are all different and in the correct order.
// This is to prevent flakiness caused by the notifications being created in such quick succession that the timestamps are the same.
fakeClock.Advance(50 * time.Millisecond)
time.Sleep(50 * time.Millisecond)
// Create the notification in the backend.
if n.globalNotification != nil {
created, err := srv.Auth().CreateGlobalNotification(ctx, n.globalNotification)
notificationIdMap[created.GetSpec().GetNotification().GetMetadata().GetLabels()[types.NotificationTitleLabel]] = created.GetMetadata().GetName()
require.NoError(t, err)
continue
}
created, err := srv.Auth().CreateUserNotification(ctx, n.userNotification)
notificationIdMap[created.GetMetadata().GetLabels()[types.NotificationTitleLabel]] = created.GetMetadata().GetName()
require.NoError(t, err)
}
// Test fetching notifications for auditor.
auditorClient, err := srv.NewClient(TestUser(auditorUsername))
require.NoError(t, err)
defer auditorClient.Close()
auditorExpectedNotifications := []string{"auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-5,manager-2", "auditor-4", "auditor-3", "auditor-2", "auditor-1"}
var finalOut []*notificationsv1.Notification
// Upsert auditor's last seen timestamp.
lastSeenTimestamp := timestamppb.New(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
_, err = auditorClient.UpsertUserLastSeenNotification(ctx, auditorUsername, &notificationsv1.UserLastSeenNotification{
Status: &notificationsv1.UserLastSeenNotificationStatus{
LastSeenTime: lastSeenTimestamp,
},
})
require.NoError(t, err)
// Fetch a page of 3 notifications.
resp, err := auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 3,
})
require.NoError(t, err)
require.Equal(t, auditorExpectedNotifications[:3], notificationsToTitlesList(t, resp.Notifications))
finalOut = append(finalOut, resp.Notifications...)
// Verify that the nextKeys are correct.
expectedUserNotifsNextKey := notificationIdMap["auditor-4"]
expectedGlobalNotifsNextKey := notificationIdMap["auditor-5,manager-2"]
expectedNextKeys := fmt.Sprintf("%s,%s",
expectedUserNotifsNextKey,
expectedGlobalNotifsNextKey) // "<auditor-4 notification id>,<auditor-5,manager-2 notification id>"
require.Equal(t, expectedNextKeys, resp.NextPageToken)
require.Equal(t, lastSeenTimestamp.GetSeconds(), resp.UserLastSeenNotificationTimestamp.GetSeconds())
// Fetch the next 3 notifications, starting from the previously received startKeys.
// After this fetch, there should be no more global notifications for auditor, so the next page token
// for global notifications should be "end".
resp, err = auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 3,
PageToken: resp.NextPageToken,
})
require.NoError(t, err)
expectedNextKeys = fmt.Sprintf("%s,", notificationIdMap["auditor-2"])
require.Equal(t, expectedNextKeys, resp.NextPageToken)
finalOut = append(finalOut, resp.Notifications...)
// Fetch the rest of the notifications, starting from the previously received startKeys.
resp, err = auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
PageToken: resp.NextPageToken,
})
require.NoError(t, err)
finalOut = append(finalOut, resp.Notifications...)
// Verify that all the notifications are in the list and in correct order.
require.Equal(t, auditorExpectedNotifications, notificationsToTitlesList(t, finalOut))
// Verify that we've reached the end of both lists.
require.Equal(t, "", resp.NextPageToken)
// Mark "auditor-2" and "auditor-5,manager-2" as dismissed.
_, err = auditorClient.UpsertUserNotificationState(ctx, auditorUsername, &notificationsv1.UserNotificationState{
Spec: &notificationsv1.UserNotificationStateSpec{
NotificationId: notificationIdMap["auditor-2"],
},
Status: &notificationsv1.UserNotificationStateStatus{
NotificationState: notificationsv1.NotificationState_NOTIFICATION_STATE_DISMISSED,
},
})
require.NoError(t, err)
_, err = auditorClient.UpsertUserNotificationState(ctx, auditorUsername, &notificationsv1.UserNotificationState{
Spec: &notificationsv1.UserNotificationStateSpec{
NotificationId: notificationIdMap["auditor-5,manager-2"],
},
Status: &notificationsv1.UserNotificationStateStatus{
NotificationState: notificationsv1.NotificationState_NOTIFICATION_STATE_DISMISSED,
},
})
require.NoError(t, err)
// Fetch notifications again and verify that the dismissed notifications are not returned.
resp, err = auditorClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
})
auditorExpectedNotifsAfterDismissal := []string{"auditor-8,manager-6", "auditor-7", "auditor-6", "auditor-4", "auditor-3", "auditor-1"}
require.NoError(t, err)
require.Equal(t, auditorExpectedNotifsAfterDismissal, notificationsToTitlesList(t, resp.Notifications))
// Test fetching notifications for manager.
managerClient, err := srv.NewClient(TestUser(managerUsername))
require.NoError(t, err)
defer managerClient.Close()
managerExpectedNotifications := []string{"auditor-8,manager-6", "manager-5", "manager-4", "manager-3", "auditor-5,manager-2", "manager-1"}
resp, err = managerClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
})
require.NoError(t, err)
require.Equal(t, managerExpectedNotifications, notificationsToTitlesList(t, resp.Notifications))
// Verify that we've reached the end of both lists.
require.Equal(t, "", resp.NextPageToken)
// Mark "auditor-8,manager-6" as clicked.
_, err = managerClient.UpsertUserNotificationState(ctx, managerUsername, &notificationsv1.UserNotificationState{
Spec: &notificationsv1.UserNotificationStateSpec{
NotificationId: notificationIdMap["auditor-8,manager-6"],
},
Status: &notificationsv1.UserNotificationStateStatus{
NotificationState: notificationsv1.NotificationState_NOTIFICATION_STATE_CLICKED,
},
})
require.NoError(t, err)
// Fetch notifications again and expect it to have the clicked label.
resp, err = managerClient.ListNotifications(ctx, &notificationsv1.ListNotificationsRequest{
PageSize: 10,
})
require.NoError(t, err)
clickedNotification := resp.Notifications[0] // "auditor-8,manager-6" is the first item in the list
clickedLabelValue := clickedNotification.GetMetadata().GetLabels()[types.NotificationClickedLabel]
require.Equal(t, "true", clickedLabelValue)
// Verify that manager can't upsert a notification state for auditor
_, err = managerClient.UpsertUserNotificationState(ctx, auditorUsername, &notificationsv1.UserNotificationState{
Spec: &notificationsv1.UserNotificationStateSpec{
NotificationId: notificationIdMap["auditor-7"],
},
Status: &notificationsv1.UserNotificationStateStatus{
NotificationState: notificationsv1.NotificationState_NOTIFICATION_STATE_DISMISSED,
},
})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err), "got error %T, expected an access denied error due to manager trying to upsert a notification state for a different user", err)
// Verify that manager can't uspert a user last seen notification for auditor.
_, err = managerClient.UpsertUserLastSeenNotification(ctx, auditorUsername, &notificationsv1.UserLastSeenNotification{
Status: &notificationsv1.UserLastSeenNotificationStatus{
LastSeenTime: lastSeenTimestamp,
},
})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err), "got error %T, expected an access denied error due to manager trying to upsert a last seen notification timestamp for a different user", err)
}
func newUserNotification(t *testing.T, username string, title string) *notificationsv1.Notification {
t.Helper()
notification := notificationsv1.Notification{
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{
Username: username,
},
Metadata: &headerv1.Metadata{
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
}
return &notification
}
func newGlobalNotification(t *testing.T, title string, spec *notificationsv1.GlobalNotificationSpec) *notificationsv1.GlobalNotification {
t.Helper()
spec.Notification = &notificationsv1.Notification{
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
}
notification := notificationsv1.GlobalNotification{
Spec: spec,
}
return &notification
}
// notificationsToTitlesList accepts a list of notifications notifications and returns a slice of strings containing their titles in order, this is used to compare against
// the expected outputs.
func notificationsToTitlesList(t *testing.T, notifications []*notificationsv1.Notification) []string {
t.Helper()
var descriptions []string
for _, notif := range notifications {
description := notif.GetMetadata().GetLabels()[types.NotificationTitleLabel]
descriptions = append(descriptions, description)
}
return descriptions
}
@@ -0,0 +1,446 @@
// Copyright 2024 Gravitational, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notifications
import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
apidefaults "github.com/gravitational/teleport/api/defaults"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
"github.com/gravitational/teleport/api/internalutils/stream"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/services"
)
// ServiceConfig holds configuration options for the notifications gRPC service.
type ServiceConfig struct {
// Backend is the interface containing all methods for backend read/writes.
Backend Backend
// Authorizer is the authorizer used to check access to resources.
Authorizer authz.Authorizer
// UserNotificationCache is a custom cache for user-specific notifications,
// this is to allow fetching notifications by date in descending order.
UserNotificationCache *services.UserNotificationCache
// GlobalNotificationCache is a custom cache for user-specific notifications,
// this is to allow fetching notifications by date in descending order.
GlobalNotificationCache *services.GlobalNotificationCache
}
// Backend contains the getters required for notification states and user last seen notifications,
// as well as the methods required by the ReviewPermissionChecker.
type Backend interface {
services.Notifications
// Needed by the ReviewPermissionChecker
services.UserLoginStatesGetter
services.UserGetter
services.RoleGetter
client.ListResourcesClient
GetRoles(ctx context.Context) ([]types.Role, error)
GetClusterName(opts ...services.MarshalOption) (types.ClusterName, error)
}
// Service implements the teleport.notifications.v1.NotificationsService RPC Service.
type Service struct {
notificationsv1.UnimplementedNotificationServiceServer
authorizer authz.Authorizer
backend Backend
userNotificationCache *services.UserNotificationCache
globalNotificationCache *services.GlobalNotificationCache
}
// NewService returns a new notifications gRPC service.
func NewService(cfg ServiceConfig) (*Service, error) {
switch {
case cfg.Backend == nil:
return nil, trace.BadParameter("backend is required")
case cfg.Authorizer == nil:
return nil, trace.BadParameter("authorizer is required")
case cfg.UserNotificationCache == nil:
return nil, trace.BadParameter("user notification cache is required")
case cfg.GlobalNotificationCache == nil:
return nil, trace.BadParameter("global notification cache is required")
}
return &Service{
authorizer: cfg.Authorizer,
backend: cfg.Backend,
userNotificationCache: cfg.UserNotificationCache,
globalNotificationCache: cfg.GlobalNotificationCache,
}, nil
}
// ListNotifications returns a paginated list of notifications which match the user.
func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
username := authCtx.User.GetName()
// Fetch all of the user's notification states. We do this upfront to filter out dismissed notifications.
notificationStatesMap := make(map[string]notificationsv1.NotificationState)
for startKey := ""; ; {
notificationStates, nextKey, err := s.backend.ListUserNotificationStates(ctx, username, apidefaults.DefaultChunkSize, startKey)
if err != nil {
return nil, trace.Wrap(err)
}
for _, notificationState := range notificationStates {
if notificationState.Spec != nil && notificationState.Status != nil {
notificationStatesMap[notificationState.Spec.NotificationId] = notificationState.Status.GetNotificationState()
}
}
if nextKey == "" {
break
}
startKey = nextKey
}
var userNotifMatchFn = func(n *notificationsv1.Notification) bool {
// Return true if the user hasn't dismissed this notification
return notificationStatesMap[n.GetMetadata().GetName()] != notificationsv1.NotificationState_NOTIFICATION_STATE_DISMISSED
}
userKey, globalKey, found := strings.Cut(req.PageToken, ",")
if !found && req.PageToken != "" {
return nil, trace.BadParameter("invalid page token provided")
}
pageSize := int(req.PageSize)
userNotifsStream := stream.Slice[*notificationsv1.Notification](nil)
if userKey != "" || !found {
userNotifsStream = stream.FilterMap(
s.userNotificationCache.StreamUserNotifications(ctx, username, userKey),
func(n *notificationsv1.Notification) (*notificationsv1.Notification, bool) {
if !userNotifMatchFn(n) {
return nil, false
}
return n, true
})
}
globalNotifsStream := stream.Slice[*notificationsv1.GlobalNotification](nil)
if globalKey != "" || !found {
globalNotifsStream = stream.FilterMap(
s.globalNotificationCache.StreamGlobalNotifications(ctx, globalKey),
func(gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, bool) {
if !s.matchGlobalNotification(ctx, authCtx, gn, notificationStatesMap) {
return nil, false
}
return gn, true
})
}
notifStream := stream.MergeStreams(
userNotifsStream,
globalNotifsStream,
isMoreRecent,
func(notification *notificationsv1.Notification) *notificationsv1.Notification {
return notification
},
func(globalNotification *notificationsv1.GlobalNotification) *notificationsv1.Notification {
notification := globalNotification.GetSpec().GetNotification()
notification.Metadata.Name = globalNotification.GetMetadata().GetName()
return notification
},
)
var notifications []*notificationsv1.Notification
var nextGlobalKey, nextUserKey string
for notifStream.Next() {
item := notifStream.Item()
if item != nil {
notifications = append(notifications, item)
}
if len(notifications) == pageSize {
// The nextKeys should represent the next unconsumed items in their respective lists.
// If the last item in this page (ie. the current item in the stream) was a user-specific notification, then the userNotificationsNextKey should be the next item in the userNotifsStream, and
// the globalNotificationsNextKey should be the current (and unconsumed) item in the globalNotifsStream. And vice-versa.
if item.GetMetadata().GetLabels()[types.NotificationScope] == "user" {
nextGlobalKey = globalNotifsStream.Item().GetMetadata().GetName()
// Advance to the next user-specific notification.
ok := userNotifsStream.Next()
if ok {
// If it exists, set it as the userNotificationsNextKey, otherwise set it to ""
nextUserKey = userNotifsStream.Item().GetMetadata().GetName()
} else {
nextUserKey = ""
}
} else {
nextUserKey = userNotifsStream.Item().GetMetadata().GetName()
// Advance to the next global notification.
ok := globalNotifsStream.Next()
if ok {
// If it exists, set it as the globalNotificationsNextKey.
nextGlobalKey = globalNotifsStream.Item().GetMetadata().GetName()
} else {
nextGlobalKey = ""
}
}
break
}
}
userLastSeenNotification, err := s.backend.GetUserLastSeenNotification(ctx, username)
if err != nil && !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// Add label to indicate notifications that the user has clicked.
for _, notification := range notifications {
if (notificationStatesMap[notification.GetMetadata().GetName()]) == notificationsv1.NotificationState_NOTIFICATION_STATE_CLICKED {
notification.GetMetadata().Labels[types.NotificationClickedLabel] = "true"
}
}
var nextPageToken string
if nextUserKey != "" || nextGlobalKey != "" {
nextPageToken = fmt.Sprintf("%s,%s", nextUserKey, nextGlobalKey)
}
response := &notificationsv1.ListNotificationsResponse{
Notifications: notifications,
NextPageToken: nextPageToken,
UserLastSeenNotificationTimestamp: userLastSeenNotification.GetStatus().GetLastSeenTime(),
}
return response, nil
}
func (s *Service) matchGlobalNotification(ctx context.Context, authCtx *authz.Context, gn *notificationsv1.GlobalNotification, notificationStatesMap map[string]notificationsv1.NotificationState) bool {
// If the user has dismissed this notification, return false.
if notificationStatesMap[gn.GetMetadata().GetName()] == notificationsv1.NotificationState_NOTIFICATION_STATE_DISMISSED {
return false
}
switch matcher := gn.Spec.Matcher.(type) {
case *notificationsv1.GlobalNotificationSpec_All:
// Always return true if the matcher is "all."
return true
case *notificationsv1.GlobalNotificationSpec_ByRoles:
matcherRoles := matcher.ByRoles.GetRoles()
userRoles := authCtx.User.GetRoles()
// If MatchAllConditions is true, then userRoles must contain every role in matcherRoles.
if gn.Spec.MatchAllConditions {
for _, matcherRole := range matcherRoles {
// Return false if there is any role missing.
if !slices.Contains(userRoles, matcherRole) {
return false
}
}
return true
}
// Return true if it matches at least one matcherRole.
for _, matcherRole := range matcherRoles {
if slices.Contains(userRoles, matcherRole) {
return true
}
}
return false
case *notificationsv1.GlobalNotificationSpec_ByPermissions:
roleConditionsList := matcher.ByPermissions.GetRoleConditions()
var results []bool
for _, roleConditions := range roleConditionsList {
match, err := s.matchRoleConditions(ctx, authCtx, roleConditions)
if err != nil {
slog.WarnContext(ctx, "Encountered error while matching RoleConditions", "role_conditions", roleConditions, "error", err)
return false
}
// If MatchAllConditions is false, we can exit at the first match.
if !gn.Spec.MatchAllConditions && match {
return true
}
// If MatchAllConditions is true, we exit at the first non-match.
if gn.Spec.MatchAllConditions && !match {
return false
}
results = append(results, match)
}
// Return false if any of the roleConditions didn't match.
if gn.Spec.MatchAllConditions {
return !slices.Contains(results, false)
}
return false
}
return false
}
func (s *Service) matchRoleConditions(ctx context.Context, authCtx *authz.Context, rc *types.RoleConditions) (bool, error) {
if len(rc.Logins) > 0 {
userLogins := authCtx.Checker.GetAllLogins()
var matchedLogin bool
for _, login := range rc.Logins {
// If at least one of the logins match, this is a match.
if slices.Contains(userLogins, login) {
matchedLogin = true
break
}
}
if !matchedLogin {
return false, nil
}
}
if len(rc.Rules) > 0 {
var matchedRule bool
for _, rule := range rc.Rules {
hasAccess, err := checkAccessToRule(authCtx, rule)
if err != nil {
return false, trace.Wrap(err, "encountered unexpected error when checking access to rule")
}
// If the user has permissions for at least one of the rules, this is a match.
if hasAccess {
matchedRule = true
break
}
}
if !matchedRule {
return false, nil
}
}
if rc.ReviewRequests != nil {
identity := authCtx.Identity.GetIdentity()
checker, err := services.NewReviewPermissionChecker(
ctx,
s.backend,
authCtx.User.GetName(),
&identity,
)
if err != nil {
return false, trace.Wrap(err)
}
// unless the user has allow directives for reviewing, they will never be able to
// see any requests other than their own.
if !checker.HasAllowDirectives() {
return false, nil
}
// We instantiate a fake access request with the defined roles, this allows us to use our existing AccessReviewChecker to check if the
// user is allowed to review requests for them.
fakeAccessRequest, err := types.NewAccessRequest("fake", "fake", rc.ReviewRequests.Roles...)
if err != nil {
return false, trace.Wrap(err)
}
canReview, err := checker.CanReviewRequest(fakeAccessRequest)
if err != nil {
return false, trace.Wrap(err, "failed to evaluate request review permissions")
}
if !canReview {
return false, nil
}
}
// This RoleConditions object matches if there were no failed matches that returned prior to this.
return true, nil
}
// checkAccessToRule returns true if the user has the permissions defined in a rule.
func checkAccessToRule(authCtx *authz.Context, rule types.Rule) (bool, error) {
for _, resourceKind := range rule.Resources {
for _, verb := range rule.Verbs {
if err := authCtx.CheckAccessToKind(resourceKind, verb); err != nil {
// If the user doesn't have access for the verbs for any one of the resources in the rule, then just return false.
if trace.IsAccessDenied(err) {
return false, nil
// If the error is due to something else, then return it.
} else {
return false, trace.Wrap(err)
}
}
}
}
return true, nil
}
// isMoreRecent returns true if the userNotification is more recent than the globalNotification.
func isMoreRecent(userNotification *notificationsv1.Notification, globalNotification *notificationsv1.GlobalNotification) bool {
userNotificationTime := userNotification.GetSpec().GetCreated().AsTime()
globalNotificationTime := globalNotification.GetSpec().GetNotification().GetSpec().GetCreated().AsTime()
return userNotificationTime.After(globalNotificationTime)
}
// UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification.
func (s *Service) UpsertUserNotificationState(ctx context.Context, req *notificationsv1.UpsertUserNotificationStateRequest) (*notificationsv1.UserNotificationState, error) {
if req.Username == "" {
return nil, trace.BadParameter("missing username")
}
if req.UserNotificationState == nil {
return nil, trace.BadParameter("missing notification state")
}
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
username := authCtx.User.GetName()
if username != req.Username {
return nil, trace.AccessDenied("a user may only update their own notification state")
}
out, err := s.backend.UpsertUserNotificationState(ctx, req.Username, req.UserNotificationState)
if err != nil {
return nil, trace.Wrap(err)
}
return out, nil
}
// UpsertUserLastSeenNotification creates or updates a user's last seen notification timestamp.
func (s *Service) UpsertUserLastSeenNotification(ctx context.Context, req *notificationsv1.UpsertUserLastSeenNotificationRequest) (*notificationsv1.UserLastSeenNotification, error) {
if req.Username == "" {
return nil, trace.BadParameter("missing username")
}
if req.UserLastSeenNotification == nil {
return nil, trace.BadParameter("missing user last seen notification")
}
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
username := authCtx.User.GetName()
if username != req.Username {
return nil, trace.AccessDenied("a user may only update their own last seen notification timestamp")
}
out, err := s.backend.UpsertUserLastSeenNotification(ctx, req.Username, req.UserLastSeenNotification)
if err != nil {
return nil, trace.Wrap(err)
}
return out, nil
}
+3 -3
View File
@@ -3626,14 +3626,14 @@ func newUserNotification(t *testing.T, name string) *notificationsv1.Notificatio
Username: name,
},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": "test-description"},
Labels: map[string]string{types.NotificationTitleLabel: "test-title"},
},
}
return notification
}
func newGlobalNotification(t *testing.T, description string) *notificationsv1.GlobalNotification {
func newGlobalNotification(t *testing.T, title string) *notificationsv1.GlobalNotification {
t.Helper()
notification := &notificationsv1.GlobalNotification{
@@ -3645,7 +3645,7 @@ func newGlobalNotification(t *testing.T, description string) *notificationsv1.Gl
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": description},
Labels: map[string]string{types.NotificationTitleLabel: title},
},
},
},
+2 -2
View File
@@ -3044,7 +3044,7 @@ func (userNotificationExecutor) delete(ctx context.Context, cache *Cache, resour
}
username := notification.GetSpec().GetUsername()
notificationId := notification.GetSpec().GetId()
notificationId := notification.GetMetadata().GetName()
err := cache.notificationsCache.DeleteUserNotification(ctx, username, notificationId)
return trace.Wrap(err)
@@ -3107,7 +3107,7 @@ func (globalNotificationExecutor) delete(ctx context.Context, cache *Cache, reso
return trace.BadParameter("unknown Notification type, expected *notificationsv1.GlobalNotification, got %T", resource)
}
notificationId := globalNotification.GetSpec().GetNotification().GetSpec().GetId()
notificationId := globalNotification.GetMetadata().GetName()
err := cache.notificationsCache.DeleteGlobalNotification(ctx, notificationId)
return trace.Wrap(err)
+1 -4
View File
@@ -2049,7 +2049,6 @@ func (p *userNotificationParser) parse(event backend.Event) (types.Resource, err
Version: types.V1,
Spec: &notificationsv1.NotificationSpec{
Username: parts[2],
Id: parts[3],
},
Metadata: &headerv1.Metadata{
Name: parts[3],
@@ -2100,9 +2099,7 @@ func (p *globalNotificationParser) parse(event backend.Event) (types.Resource, e
Version: types.V1,
Spec: &notificationsv1.GlobalNotificationSpec{
Notification: &notificationsv1.Notification{
Spec: &notificationsv1.NotificationSpec{
Id: parts[2],
},
Spec: &notificationsv1.NotificationSpec{},
},
},
Metadata: &headerv1.Metadata{
+24 -12
View File
@@ -140,10 +140,7 @@ func (s *NotificationsService) CreateUserNotification(ctx context.Context, notif
if err != nil {
return nil, trace.Wrap(err)
}
notification.Spec.Id = uuid.String()
// We set this to the UUID because the service adapter uses `getName()` to determine the backend key to use when storing the notification.
notification.Metadata.Name = notification.Spec.Id
notification.Metadata.Name = uuid.String()
if err := CheckAndSetExpiry(notification, s.clock); err != nil {
return nil, trace.Wrap(err)
@@ -151,6 +148,8 @@ func (s *NotificationsService) CreateUserNotification(ctx context.Context, notif
notification.Spec.Created = timestamppb.New(s.clock.Now())
notification.Metadata.Labels[types.NotificationScope] = "user"
// Append username prefix.
serviceWithPrefix := s.userNotificationService.WithPrefix(notification.Spec.Username)
@@ -165,7 +164,7 @@ func (s *NotificationsService) UpsertUserNotification(ctx context.Context, notif
}
// Precautionary check in case of accidental misuse.
if notification.Spec.Id == "" {
if notification.Metadata.Name == "" {
return nil, trace.BadParameter("notification id is missing. Did you mean to use CreateUserNotification?")
}
@@ -230,10 +229,8 @@ func (s *NotificationsService) CreateGlobalNotification(ctx context.Context, glo
if err != nil {
return nil, trace.Wrap(err)
}
globalNotification.Spec.Notification.Spec.Id = uuid.String()
// We set this to the UUID because the service adapter uses `getName()` to determine the backend key to use when storing the notification.
globalNotification.Metadata = &headerv1.Metadata{Name: globalNotification.Spec.Notification.Spec.Id}
globalNotification.Metadata = &headerv1.Metadata{Name: uuid.String()}
globalNotification.Spec.Notification.Metadata.Name = uuid.String()
if err := CheckAndSetExpiry(globalNotification.Spec.Notification, s.clock); err != nil {
return nil, trace.Wrap(err)
@@ -241,6 +238,8 @@ func (s *NotificationsService) CreateGlobalNotification(ctx context.Context, glo
globalNotification.Spec.Notification.Spec.Created = timestamppb.New(s.clock.Now())
globalNotification.Spec.Notification.Metadata.Labels[types.NotificationScope] = "global"
created, err := s.globalNotificationService.CreateResource(ctx, globalNotification)
return created, trace.Wrap(err)
}
@@ -252,7 +251,7 @@ func (s *NotificationsService) UpsertGlobalNotification(ctx context.Context, glo
}
// Precautionary check in case of accidental misuse.
if globalNotification.Spec.Notification.Spec.Id == "" {
if globalNotification.Spec.Notification.Metadata.Name == "" {
return nil, trace.BadParameter("notification id is missing. Did you mean to use CreateGlobalNotification?")
}
@@ -274,8 +273,21 @@ func (s *NotificationsService) UpsertUserNotificationState(ctx context.Context,
// Verify that the notification this state is for exists.
notifServiceWithPrefix := s.userNotificationService.WithPrefix(username)
if _, err := notifServiceWithPrefix.GetResource(ctx, state.Spec.NotificationId); err != nil {
return nil, trace.Wrap(err)
_, err := notifServiceWithPrefix.GetResource(ctx, state.Spec.NotificationId)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
// If we didn't find a user-specific notification with this ID, try finding a global notification.
_, err := s.globalNotificationService.GetResource(ctx, state.Spec.NotificationId)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
}
return nil, trace.NotFound("notification '%s' does not exist", state.Spec.NotificationId)
}
}
state.Kind = types.KindUserNotificationState
+15 -10
View File
@@ -32,6 +32,7 @@ import (
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/memory"
)
@@ -60,14 +61,14 @@ func TestUserNotificationCRUD(t *testing.T) {
notification, err := service.CreateUserNotification(ctx, userNotification1)
require.NoError(t, err)
require.Empty(t, cmp.Diff(userNotification1, notification, protocmp.Transform()))
notification1Id := notification.Spec.Id
notification1Id := notification.GetMetadata().GetName()
// Prevent flakiness caused by notifications being created too close one after the other, which causes their UUID timestamps to be the same
// and the lexicographical ordering to possibly be wrong as it then relies on the random section of the UUID.
time.Sleep(250 * time.Millisecond)
notification, err = service.CreateUserNotification(ctx, userNotification2)
require.NoError(t, err)
require.Empty(t, cmp.Diff(userNotification2, notification, protocmp.Transform()))
notification2Id := notification.Spec.Id
notification2Id := notification.GetMetadata().GetName()
// Test deleting a notification.
err = service.DeleteUserNotification(ctx, testUsername, notification1Id)
@@ -87,7 +88,7 @@ func TestUserNotificationCRUD(t *testing.T) {
userNotification1 = newUserNotification(t, testUsername, "test-notification-1")
_, err = service.CreateUserNotification(ctx, userNotification1)
require.NoError(t, err)
notification1Id = notification.Spec.Id
notification1Id = notification.GetMetadata().GetName()
err = service.DeleteAllUserNotificationsForUser(ctx, testUsername)
require.NoError(t, err)
// Verify that the notifications don't exist anymore by attempting to delete them.
@@ -148,7 +149,7 @@ func TestGlobalNotificationCRUD(t *testing.T) {
notification, err := service.CreateGlobalNotification(ctx, globalNotification1)
require.NoError(t, err)
require.Empty(t, cmp.Diff(globalNotification1, notification, protocmp.Transform()))
globalNotification1Id := notification.Spec.Notification.Spec.Id
globalNotification1Id := notification.GetMetadata().GetName()
notification, err = service.CreateGlobalNotification(ctx, globalNotification2)
require.NoError(t, err)
require.Empty(t, cmp.Diff(globalNotification2, notification, protocmp.Transform()))
@@ -188,13 +189,13 @@ func TestUserNotificationStateCRUD(t *testing.T) {
userNotification2 := newUserNotification(t, testUsername, "test-notification-2")
notification, err := service.CreateUserNotification(ctx, userNotification1)
require.NoError(t, err)
notification1Id := notification.Spec.Id
notification1Id := notification.GetMetadata().GetName()
// Prevent flakiness caused by notifications being created too close one after the other, which causes their UUID timestamps to be the same
// and the lexicographical ordering to possibly be wrong as it then relies on the random section of the UUID.
time.Sleep(250 * time.Millisecond)
notification, err = service.CreateUserNotification(ctx, userNotification2)
require.NoError(t, err)
notification2Id := notification.Spec.Id
notification2Id := notification.GetMetadata().GetName()
userNotificationState1 := &notificationsv1.UserNotificationState{
Spec: &notificationsv1.UserNotificationStateSpec{
@@ -374,7 +375,7 @@ func TestUserLastSeenNotificationCRUD(t *testing.T) {
require.True(t, trace.IsNotFound(err), "got error %T, expected a not found error due to user_last_seen_notification for test-username not existing", err)
}
func newUserNotification(t *testing.T, username string, description string) *notificationsv1.Notification {
func newUserNotification(t *testing.T, username string, title string) *notificationsv1.Notification {
t.Helper()
notification := notificationsv1.Notification{
@@ -383,14 +384,16 @@ func newUserNotification(t *testing.T, username string, description string) *not
Username: username,
},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": description},
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
}
return &notification
}
func newGlobalNotification(t *testing.T, description string) *notificationsv1.GlobalNotification {
func newGlobalNotification(t *testing.T, title string) *notificationsv1.GlobalNotification {
t.Helper()
notification := notificationsv1.GlobalNotification{
@@ -402,7 +405,9 @@ func newGlobalNotification(t *testing.T, description string) *notificationsv1.Gl
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": description},
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
},
},
+3 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/gravitational/trace"
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
"github.com/gravitational/teleport/api/types"
)
// Notifications defines an interface for managing notifications.
@@ -62,8 +63,8 @@ func ValidateNotification(notification *notificationsv1.Notification) error {
return trace.BadParameter("notification metadata is missing")
}
if notification.Metadata.Labels == nil {
return trace.BadParameter("notification metadata labels are missing")
if _, exists := notification.Metadata.GetLabels()[types.NotificationTitleLabel]; !exists {
return trace.BadParameter("notification title label is missing")
}
return nil
+4 -1
View File
@@ -205,6 +205,9 @@ func (c *UserNotificationCache) StreamUserNotifications(ctx context.Context, use
// Get the initial startKey if it wasn't provided.
if startKey == "" {
startKey = sortcache.NextKey(endKey)
} else {
// The sortcache expects the key to be in <username>/<uuid> format, so we prepend the username since the startKey passed into this function will just be a UUID.
startKey = fmt.Sprintf("%s/%s", username, startKey)
}
var done bool
@@ -266,7 +269,7 @@ func (c *UserNotificationCache) fetch(ctx context.Context) (*sortcache.SortCache
// GetUserSpecificKey returns the key for a user-specific notification in <username>/<notification uuid> format.
func GetUserSpecificKey(n *notificationsv1.Notification) string {
username := n.GetSpec().GetUsername()
id := n.GetSpec().GetId()
id := n.GetMetadata().GetName()
return fmt.Sprintf("%s/%s", username, id)
}
+62 -58
View File
@@ -109,50 +109,50 @@ func TestUserNotificationsCache(t *testing.T) {
usernameB := "bob"
// Describe a set of mock user notifications to be created.
// The number in the description represents the order in which they are created, eg. alice-1 is the first (and thus oldest) notification to be created for alice.
// The number in the title represents the order in which they are created, eg. alice-1 is the first (and thus oldest) notification to be created for alice.
testNotifications := []struct {
username string
description string
username string
title string
}{
{
username: usernameB,
description: "bob-1",
username: usernameB,
title: "bob-1",
},
{
username: usernameB,
description: "bob-2",
username: usernameB,
title: "bob-2",
},
{
username: usernameA,
description: "alice-1",
username: usernameA,
title: "alice-1",
},
{
username: usernameB,
description: "bob-3",
username: usernameB,
title: "bob-3",
},
{
username: usernameB,
description: "bob-4",
username: usernameB,
title: "bob-4",
},
{
username: usernameA,
description: "alice-2",
username: usernameA,
title: "alice-2",
},
{
username: usernameA,
description: "alice-3",
username: usernameA,
title: "alice-3",
},
{
username: usernameA,
description: "alice-4",
username: usernameA,
title: "alice-4",
},
{
username: usernameB,
description: "bob-5",
username: usernameB,
title: "bob-5",
},
{
username: usernameA,
description: "alice-5",
username: usernameA,
title: "alice-5",
},
}
@@ -164,7 +164,7 @@ func TestUserNotificationsCache(t *testing.T) {
for _, n := range testNotifications {
// We add a delay to ensure that the timestamps in the generated UUID's are all different and in the correct order.
time.Sleep(50 * time.Millisecond)
notification := newUserNotification(t, n.username, n.description)
notification := newUserNotification(t, n.username, n.title)
// Create the notification in the backend.
created, err := svcs.CreateUserNotification(ctx, notification)
require.NoError(t, err)
@@ -205,41 +205,41 @@ func TestUserNotificationsCache(t *testing.T) {
streamA.Next()
streamA.Next()
streamA.Next()
usernameAThirdItemStartKey = services.GetUserSpecificKey(streamA.Item())
usernameAThirdItemStartKey = streamA.Item().GetMetadata().GetName()
var usernameBThirdItemStartKey string
streamB := cache.StreamUserNotifications(ctx, usernameB, "")
streamB.Next()
streamB.Next()
streamB.Next()
usernameBThirdItemStartKey = services.GetUserSpecificKey(streamB.Item())
usernameBThirdItemStartKey = streamB.Item().GetMetadata().GetName()
testCases := []struct {
testName string
username string
startKey string
expectedNotificationDescriptions []string
testName string
username string
startKey string
expectedNotificationTitles []string
}{
{
testName: "correctly fetches sorted notifications for usernameA with no startKey",
username: usernameA,
// Since alice-1 is the oldest, it should be at the end of the list.
expectedNotificationDescriptions: []string{"alice-5", "alice-4", "alice-3", "alice-2", "alice-1"},
expectedNotificationTitles: []string{"alice-5", "alice-4", "alice-3", "alice-2", "alice-1"},
},
{
testName: "correctly fetches sorted notifications for usernameB with no startKey",
username: usernameB,
expectedNotificationDescriptions: []string{"bob-5", "bob-4", "bob-3", "bob-2", "bob-1"},
testName: "correctly fetches sorted notifications for usernameB with no startKey",
username: usernameB,
expectedNotificationTitles: []string{"bob-5", "bob-4", "bob-3", "bob-2", "bob-1"},
}, {
testName: "correctly fetches sorted notifications for usernameA with a startKey",
username: usernameA,
startKey: usernameAThirdItemStartKey,
expectedNotificationDescriptions: []string{"alice-3", "alice-2", "alice-1"},
testName: "correctly fetches sorted notifications for usernameA with a startKey",
username: usernameA,
startKey: usernameAThirdItemStartKey,
expectedNotificationTitles: []string{"alice-3", "alice-2", "alice-1"},
}, {
testName: "correctly fetches sorted notifications for usernameB with a startKey",
username: usernameB,
startKey: usernameBThirdItemStartKey,
expectedNotificationDescriptions: []string{"bob-3", "bob-2", "bob-1"},
testName: "correctly fetches sorted notifications for usernameB with a startKey",
username: usernameB,
startKey: usernameBThirdItemStartKey,
expectedNotificationTitles: []string{"bob-3", "bob-2", "bob-1"},
},
}
@@ -248,11 +248,11 @@ func TestUserNotificationsCache(t *testing.T) {
var out []string
notifsStream := cache.StreamUserNotifications(ctx, tc.username, tc.startKey)
for notifsStream.Next() {
desc := notifsStream.Item().GetMetadata().GetLabels()["description"]
out = append(out, desc)
title := notifsStream.Item().GetMetadata().GetLabels()[types.NotificationTitleLabel]
out = append(out, title)
}
notifsStream.Done()
require.Equal(t, tc.expectedNotificationDescriptions, out)
require.Equal(t, tc.expectedNotificationTitles, out)
})
}
@@ -303,8 +303,8 @@ func TestUserNotificationsCache(t *testing.T) {
expected := []string{"alice-3", "alice-2", "alice-1"}
notifsStream := cache.StreamUserNotifications(ctx, usernameA, "")
for notifsStream.Next() {
desc := notifsStream.Item().GetMetadata().GetLabels()["description"]
out = append(out, desc)
title := notifsStream.Item().GetMetadata().GetLabels()[types.NotificationTitleLabel]
out = append(out, title)
}
notifsStream.Done()
require.Equal(t, expected, out)
@@ -320,7 +320,7 @@ func TestGlobalNotificationsCache(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testNotificationDescriptions := []string{
testNotificationTitles := []string{
"gn-1",
"gn-2",
"gn-3",
@@ -332,7 +332,7 @@ func TestGlobalNotificationsCache(t *testing.T) {
var notificationIds []string
// Create the notifications, with a 50ms delay between each one.
for _, gn := range testNotificationDescriptions {
for _, gn := range testNotificationTitles {
time.Sleep(50 * time.Millisecond)
notification := newGlobalNotification(t, gn)
// Create the notification in the backend.
@@ -350,7 +350,7 @@ func TestGlobalNotificationsCache(t *testing.T) {
require.NoError(t, err)
gnStream.Done()
if len(collectedStream) == len(testNotificationDescriptions) {
if len(collectedStream) == len(testNotificationTitles) {
break
}
@@ -375,8 +375,8 @@ func TestGlobalNotificationsCache(t *testing.T) {
expected := []string{"gn-5", "gn-4", "gn-3", "gn-2", "gn-1"}
gnStream = cache.StreamGlobalNotifications(ctx, "")
for gnStream.Next() {
desc := gnStream.Item().GetSpec().GetNotification().GetMetadata().GetLabels()["description"]
out = append(out, desc)
title := gnStream.Item().GetSpec().GetNotification().GetMetadata().GetLabels()[types.NotificationTitleLabel]
out = append(out, title)
}
require.Equal(t, expected, out)
gnStream.Done()
@@ -386,8 +386,8 @@ func TestGlobalNotificationsCache(t *testing.T) {
expected = []string{"gn-3", "gn-2", "gn-1"}
gnStream = cache.StreamGlobalNotifications(ctx, thirdItemStartKey)
for gnStream.Next() {
desc := gnStream.Item().GetSpec().GetNotification().GetMetadata().GetLabels()["description"]
out = append(out, desc)
title := gnStream.Item().GetSpec().GetNotification().GetMetadata().GetLabels()[types.NotificationTitleLabel]
out = append(out, title)
}
require.Equal(t, expected, out)
gnStream.Done()
@@ -420,7 +420,7 @@ func TestGlobalNotificationsCache(t *testing.T) {
}
}
func newUserNotification(t *testing.T, username string, description string) *notificationsv1.Notification {
func newUserNotification(t *testing.T, username string, title string) *notificationsv1.Notification {
t.Helper()
notification := notificationsv1.Notification{
@@ -429,14 +429,16 @@ func newUserNotification(t *testing.T, username string, description string) *not
Username: username,
},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": description},
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
}
return &notification
}
func newGlobalNotification(t *testing.T, description string) *notificationsv1.GlobalNotification {
func newGlobalNotification(t *testing.T, title string) *notificationsv1.GlobalNotification {
t.Helper()
notification := notificationsv1.GlobalNotification{
@@ -448,7 +450,9 @@ func newGlobalNotification(t *testing.T, description string) *notificationsv1.Gl
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": description},
Labels: map[string]string{
types.NotificationTitleLabel: title,
},
},
},
},
+6 -8
View File
@@ -38,11 +38,10 @@ func TestMarshalNotificationRoundTrip(t *testing.T) {
Kind: types.KindNotification,
Version: types.V1,
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{
Id: "test-notification-1",
},
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": "description-1"},
Name: "test-notification-1",
Labels: map[string]string{types.NotificationTitleLabel: "title-1"},
},
}
@@ -67,11 +66,10 @@ func TestMarshalGlobalNotificationRoundTrip(t *testing.T) {
},
Notification: &notificationsv1.Notification{
SubKind: "test-subkind",
Spec: &notificationsv1.NotificationSpec{
Id: "test-notification-id",
},
Spec: &notificationsv1.NotificationSpec{},
Metadata: &headerv1.Metadata{
Labels: map[string]string{"description": "description-1"},
Name: "test-notification-id",
Labels: map[string]string{types.NotificationTitleLabel: "title-1"},
},
},
},