Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tctl notifications commands #42124

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4805,6 +4805,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 @@ -1043,6 +1043,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 @@ -27,6 +27,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 @@ -106,6 +107,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 @@ -481,3 +493,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)
}

zmb3 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -178,6 +178,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindVnetConfig, RW()),
types.NewRule(types.KindBotInstance, RW()),
types.NewRule(types.KindAccessGraphSettings, RW()),
types.NewRule(types.KindNotification, RW()),
},
},
},
Expand Down Expand Up @@ -284,6 +285,7 @@ func NewPresetAuditorRole() types.Role {
types.NewRule(types.KindSecurityReport, append(RO(), types.VerbUse)),
types.NewRule(types.KindAuditQuery, append(RO(), types.VerbUse)),
types.NewRule(types.KindBotInstance, RO()),
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
Loading