Skip to content

Commit

Permalink
Add tctl notifications commands (#42124) (#45503)
Browse files Browse the repository at this point in the history
  • Loading branch information
rudream authored Aug 15, 2024
1 parent e3a159f commit 6962f77
Show file tree
Hide file tree
Showing 16 changed files with 1,019 additions and 202 deletions.
24 changes: 24 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4863,6 +4863,30 @@ func (c *Client) ListNotifications(ctx context.Context, req *notificationsv1pb.L
return rsp, trace.Wrap(err)
}

// CreateGlobalNotification creates a global notification.
func (c *Client) CreateGlobalNotification(ctx context.Context, req *notificationsv1pb.CreateGlobalNotificationRequest) (*notificationsv1pb.GlobalNotification, error) {
rsp, err := c.NotificationServiceClient().CreateGlobalNotification(ctx, req)
return rsp, trace.Wrap(err)
}

// CreateUserNotification creates a user-specific notification.
func (c *Client) CreateUserNotification(ctx context.Context, req *notificationsv1pb.CreateUserNotificationRequest) (*notificationsv1pb.Notification, error) {
rsp, err := c.NotificationServiceClient().CreateUserNotification(ctx, req)
return rsp, trace.Wrap(err)
}

// DeleteGlobalNotification deletes a global notification.
func (c *Client) DeleteGlobalNotification(ctx context.Context, req *notificationsv1pb.DeleteGlobalNotificationRequest) error {
_, err := c.NotificationServiceClient().DeleteGlobalNotification(ctx, req)
return trace.Wrap(err)
}

// DeleteUserNotification not implemented: can only be called locally.
func (c *Client) DeleteUserNotification(ctx context.Context, req *notificationsv1pb.DeleteUserNotificationRequest) error {
_, err := c.NotificationServiceClient().DeleteUserNotification(ctx, req)
return trace.Wrap(err)
}

// UpsertUserNotificationState creates or updates a user notification state which records whether the user has clicked on or dismissed a notification.
func (c *Client) UpsertUserNotificationState(ctx context.Context, req *notificationsv1pb.UpsertUserNotificationStateRequest) (*notificationsv1pb.UserNotificationState, error) {
rsp, err := c.NotificationServiceClient().UpsertUserNotificationState(ctx, req)
Expand Down
447 changes: 275 additions & 172 deletions api/gen/proto/go/teleport/notifications/v1/notifications_service.pb.go

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions api/proto/teleport/notifications/v1/notifications_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ message ListNotificationsRequest {
int32 page_size = 1;
// page_token is the next_page_token value returned from a previous ListUserNotifications request, if any.
string page_token = 2;
// filters specify search criteria to limit which notifications should be returned. If omitted, the default behavior will be to list all notifications.
NotificationFilters filters = 3;
}

// NotificationFilters provide a mechanism to refine ListNotification results.
message NotificationFilters {
// username is the username of the user the notifications being listed are for.
string username = 1;
// global_only is whether to only list global notifications (notifications capable of targetting multiple users).
bool global_only = 2;
// user_created_only is whether to only list user-created notifications (ie. notifications created by an admin via the tctl interface).
bool user_created_only = 3;
}

// ListNotificationsResponse is the response from listing a user's notifications.
Expand Down
2 changes: 2 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,8 @@ const (
NotificationClickedLabel = TeleportInternalLabelPrefix + "clicked"
// NotificationScope is the label which contains the scope of the notification, either "user" or "global"
NotificationScope = TeleportInternalLabelPrefix + "scope"
// NotificationTextContentLabel is the label which contains the text content of a user-created notification.
NotificationTextContentLabel = TeleportInternalLabelPrefix + "content"

// NotificationDefaultInformationalSubKind is the default subkind for an informational notification.
NotificationDefaultInformationalSubKind = "default-informational"
Expand Down
34 changes: 22 additions & 12 deletions lib/auth/authclient/clt.go
Original file line number Diff line number Diff line change
Expand Up @@ -712,21 +712,36 @@ func (c *Client) UpsertUserLastSeenNotification(ctx context.Context, username st
}

// CreateGlobalNotification creates a global notification.
func (c *Client) CreateGlobalNotification(ctx context.Context, globalNotification *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, error) {
// TODO(rudream): implement client methods for notifications
return nil, trace.NotImplemented(notImplementedMessage)
func (c *Client) CreateGlobalNotification(ctx context.Context, gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, error) {
rsp, err := c.APIClient.CreateGlobalNotification(ctx, &notificationsv1.CreateGlobalNotificationRequest{
GlobalNotification: gn,
})
return rsp, trace.Wrap(err)
}

// CreateUserNotification creates a user-specific notification.
func (c *Client) CreateUserNotification(ctx context.Context, notification *notificationsv1.Notification) (*notificationsv1.Notification, error) {
// TODO(rudream): implement client methods for notifications
return nil, trace.NotImplemented(notImplementedMessage)
rsp, err := c.APIClient.CreateUserNotification(ctx, &notificationsv1.CreateUserNotificationRequest{
Notification: notification,
})
return rsp, trace.Wrap(err)
}

// DeleteGlobalNotification deletes a global notification.
func (c *Client) DeleteGlobalNotification(ctx context.Context, notificationId string) error {
// TODO(rudream): implement client methods for notifications
return trace.NotImplemented(notImplementedMessage)
err := c.APIClient.DeleteGlobalNotification(ctx, &notificationsv1.DeleteGlobalNotificationRequest{
NotificationId: notificationId,
})
return trace.Wrap(err)
}

// DeleteUserNotification not implemented: can only be called locally.
func (c *Client) DeleteUserNotification(ctx context.Context, username string, notificationId string) error {
err := c.APIClient.DeleteUserNotification(ctx, &notificationsv1.DeleteUserNotificationRequest{
Username: username,
NotificationId: notificationId,
})
return trace.Wrap(err)
}

// DeleteAllGlobalNotifications not implemented: can only be called locally.
Expand Down Expand Up @@ -754,11 +769,6 @@ func (c *Client) DeleteUserLastSeenNotification(ctx context.Context, username st
return trace.NotImplemented(notImplementedMessage)
}

// DeleteUserNotification not implemented: can only be called locally.
func (c *Client) DeleteUserNotification(ctx context.Context, username string, notificationId string) error {
return trace.NotImplemented(notImplementedMessage)
}

// DeleteUserNotificationState not implemented: can only be called locally.
func (c *Client) DeleteUserNotificationState(ctx context.Context, username string, notificationId string) error {
return trace.NotImplemented(notImplementedMessage)
Expand Down
200 changes: 200 additions & 0 deletions lib/auth/notifications/notificationsv1/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"google.golang.org/protobuf/types/known/emptypb"

"github.com/gravitational/teleport/api/client"
apidefaults "github.com/gravitational/teleport/api/defaults"
Expand Down Expand Up @@ -104,6 +105,17 @@ func NewService(cfg ServiceConfig) (*Service, error) {

// ListNotifications returns a paginated list of notifications which match the user.
func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) {
if req.Filters != nil {
if req.Filters.GlobalOnly {
return s.listGlobalNotifications(ctx, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize)
}
if req.Filters.Username != "" {
return s.listUserSpecificNotificationsForUser(ctx, req.Filters.Username, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize)
}

return nil, trace.BadParameter("Invalid filters were provided, exactly one of GlobalOnly or Username must be defined.")
}

authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
Expand Down Expand Up @@ -479,3 +491,191 @@ func (s *Service) UpsertUserLastSeenNotification(ctx context.Context, req *notif

return out, nil
}

// CreateGlobalNotification creates a global notification.
func (s *Service) CreateGlobalNotification(ctx context.Context, req *notificationsv1.CreateGlobalNotificationRequest) (*notificationsv1.GlobalNotification, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil {
return nil, trace.Wrap(err)
}

out, err := s.backend.CreateGlobalNotification(ctx, req.GlobalNotification)
if err != nil {
return nil, trace.Wrap(err)
}

return out, nil
}

// CreateUserNotification creates a user-specific notification.
func (s *Service) CreateUserNotification(ctx context.Context, req *notificationsv1.CreateUserNotificationRequest) (*notificationsv1.Notification, error) {
if req.Username == "" {
return nil, trace.BadParameter("missing username")
}

authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbCreate); err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil {
return nil, trace.Wrap(err)
}

out, err := s.backend.CreateUserNotification(ctx, req.Notification)
if err != nil {
return nil, trace.Wrap(err)
}

return out, nil
}

// DeleteGlobalNotification deletes a global notification.
func (s *Service) DeleteGlobalNotification(ctx context.Context, req *notificationsv1.DeleteGlobalNotificationRequest) (*emptypb.Empty, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbDelete); err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil {
return nil, trace.Wrap(err)
}

err = s.backend.DeleteGlobalNotification(ctx, req.NotificationId)
return nil, trace.Wrap(err)
}

// DeleteUserNotification deletes a user-specific notification.
func (s *Service) DeleteUserNotification(ctx context.Context, req *notificationsv1.DeleteUserNotificationRequest) (*emptypb.Empty, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbDelete); err != nil {
return nil, trace.Wrap(err)
}

if err := authCtx.AuthorizeAdminActionAllowReusedMFA(); err != nil {
return nil, trace.Wrap(err)
}

err = s.backend.DeleteUserNotification(ctx, req.Username, req.NotificationId)
return nil, trace.Wrap(err)
}

// listUserSpecificNotificationsForUser returns a paginated list of all user-specific notifications for a user. This should only be used by admins.
func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, username string, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) {
if username == "" {
return nil, trace.BadParameter("missing username")
}

authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) {
return nil, trace.AccessDenied("only RoleAdmin can list notifications for a specific user")
}

if err := authCtx.CheckAccessToKind(types.KindNotification, types.VerbList); err != nil {
return nil, trace.Wrap(err)
}

stream := stream.FilterMap(
s.userNotificationCache.StreamUserNotifications(ctx, username, pageToken),
func(n *notificationsv1.Notification) (*notificationsv1.Notification, bool) {
// If only user-created notifications are requested, filter by the user-creatd subkinds.
if userCreatedOnly &&
n.GetSubKind() != types.NotificationUserCreatedInformationalSubKind &&
n.GetSubKind() != types.NotificationUserCreatedWarningSubKind {
return nil, false
}

return n, true
})

var notifications []*notificationsv1.Notification
var nextKey string

for stream.Next() {
item := stream.Item()
if item != nil {
notifications = append(notifications, item)
if len(notifications) == int(pageSize) {
nextKey = item.GetMetadata().GetName()
break
}
}
}

return &notificationsv1.ListNotificationsResponse{
Notifications: notifications,
NextPageToken: nextKey,
}, nil
}

// listGlobalNotifications returns a paginated list of all global notifications. This should only be used by admins.
func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

if !authz.HasBuiltinRole(*authCtx, string(types.RoleAdmin)) {
return nil, trace.AccessDenied("only RoleAdmin can list all global notifications")
}

stream := stream.FilterMap(
s.globalNotificationCache.StreamGlobalNotifications(ctx, pageToken),
func(gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, bool) {
// If only user-created notifications are requested, filter by the user-creatd subkinds.
if userCreatedOnly &&
gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedInformationalSubKind &&
gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedWarningSubKind {
return nil, false
}

return gn, true
})

var notifications []*notificationsv1.Notification
var nextKey string

for stream.Next() {
item := stream.Item()
if item != nil {
notification := item.GetSpec().GetNotification()
notification.Metadata.Name = item.GetMetadata().GetName()

notifications = append(notifications, notification)

if len(notifications) == int(pageSize) {
nextKey = item.GetMetadata().GetName()
break
}
}
}

return &notificationsv1.ListNotificationsResponse{
Notifications: notifications,
NextPageToken: nextKey,
}, nil
}
2 changes: 2 additions & 0 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindAppServer, RW()),
types.NewRule(types.KindVnetConfig, RW()),
types.NewRule(types.KindAccessGraphSettings, RW()),
types.NewRule(types.KindNotification, RW()),
},
},
},
Expand Down Expand Up @@ -282,6 +283,7 @@ func NewPresetAuditorRole() types.Role {
types.NewRule(types.KindInstance, RO()),
types.NewRule(types.KindSecurityReport, append(RO(), types.VerbUse)),
types.NewRule(types.KindAuditQuery, append(RO(), types.VerbUse)),
types.NewRule(types.KindNotification, RO()),
},
},
},
Expand Down
26 changes: 14 additions & 12 deletions lib/web/ui/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ import (
)

type Notification struct {
Id string `json:"id"`
Title string `json:"title"`
SubKind string `json:"subKind"`
Created time.Time `json:"created"`
Clicked bool `json:"clicked"`
Labels []Label `json:"labels"`
ID string `json:"id"`
Title string `json:"title"`
SubKind string `json:"subKind"`
Created time.Time `json:"created"`
Clicked bool `json:"clicked"`
TextContent string `json:"textContent,omitempty"`
Labels []Label `json:"labels"`
}

// MakeNotification creates a notification object for the WebUI.
Expand All @@ -41,11 +42,12 @@ func MakeNotification(notification *notificationsv1.Notification) Notification {
clicked := notification.Metadata.GetLabels()[types.NotificationClickedLabel] == "true"

return Notification{
Id: notification.Metadata.GetName(),
Title: notification.Metadata.GetLabels()[types.NotificationTitleLabel],
SubKind: notification.SubKind,
Created: notification.Spec.Created.AsTime(),
Clicked: clicked,
Labels: labels,
ID: notification.Metadata.GetName(),
Title: notification.Metadata.GetLabels()[types.NotificationTitleLabel],
SubKind: notification.SubKind,
Created: notification.Spec.Created.AsTime(),
Clicked: clicked,
TextContent: notification.Metadata.GetLabels()[types.NotificationTextContentLabel],
Labels: labels,
}
}
1 change: 1 addition & 0 deletions tool/tctl/common/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func Commands() []CLICommand {
&IdPCommand{},
&accessmonitoring.Command{},
&plugin.PluginsCommand{},
&NotificationCommand{},
&configure.SSOConfigureCommand{},
&tester.SSOTestCommand{},
&fido2Command{},
Expand Down
Loading

0 comments on commit 6962f77

Please sign in to comment.