Skip to content
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
3 changes: 3 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4947,6 +4947,9 @@ message GetClusterAlertsRequest {
bool WithSuperseded = 4;
// WithAcknowledged includes acknowledged alerts in the output of the request.
bool WithAcknowledged = 5;
// WithUntargeted requests that alerts be included even if they are not specifically
// targeted toward the caller. This has no effect unless the caller has `cluster_alert:list`.
bool WithUntargeted = 6;
}

// AlertAcknowledgement marks a cluster alert as having been "acknowledged".
Expand Down
1,618 changes: 827 additions & 791 deletions api/types/types.pb.go

Large diffs are not rendered by default.

46 changes: 21 additions & 25 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1157,9 +1157,13 @@ func (a *ServerWithRoles) GetClusterAlerts(ctx context.Context, query types.GetC
}
}

// admin skips rbac checks, but still obeys acks and supersessions, so
// we store the result of the check for use per-alert during filtering.
isAdmin := a.hasBuiltinRole(types.RoleAdmin)
// by default we only show alerts whose labels specify that a given user should see them, but users
// with permissions to view all resources of kind 'cluster_alert' can opt into viewing all alerts
// regardless of labels for management/debug purposes.
var resourceLevelPermit bool
if query.WithUntargeted && a.withOptions(quietAction(true)).action(apidefaults.Namespace, types.KindClusterAlert, types.VerbRead, types.VerbList) == nil {
resourceLevelPermit = true
}

// filter alerts by acks and teleport.internal 'permit' labels to determine whether the alert
// was intended to be visible to the calling user.
Expand All @@ -1173,9 +1177,9 @@ Outer:
}
}

// remaining checks in this loop are access-controls, so short-circuit
// if caller is admin.
if isAdmin {
// remaining checks in this loop are evaluating per-alert access, so short-circuit
// if we are going off of resource-level permissions for this query.
if resourceLevelPermit {
filtered = append(filtered, alert)
continue Outer
}
Expand Down Expand Up @@ -1236,43 +1240,35 @@ Outer:
}

func (a *ServerWithRoles) UpsertClusterAlert(ctx context.Context, alert types.ClusterAlert) error {
// admin-only API. the expected usage of this is mostly as something the auth server itself would do
// internally, but it is useful to be able to create alerts via tctl for testing/debug purposes.
if !a.hasBuiltinRole(types.RoleAdmin) {
return trace.AccessDenied("cluster alert creation is admin-only")
if err := a.action(apidefaults.Namespace, types.KindClusterAlert, types.VerbCreate, types.VerbUpdate); err != nil {
return trace.Wrap(err)
}

return a.authServer.UpsertClusterAlert(ctx, alert)
}

func (a *ServerWithRoles) CreateAlertAck(ctx context.Context, ack types.AlertAcknowledgement) error {
// alert acknowledgement is admin-only for now as it is a fairly niche feature,
// but we may want to develop custom rbac for this feature in the future
// if use of cluster alerts becomes more widespread.
if !a.hasBuiltinRole(types.RoleAdmin) {
return trace.AccessDenied("alert ack is admin-only")
// we treat alert acks as an extension of the cluster alert resource rather than its own resource
if err := a.action(apidefaults.Namespace, types.KindClusterAlert, types.VerbCreate, types.VerbUpdate); err != nil {
return trace.Wrap(err)
}

return a.authServer.CreateAlertAck(ctx, ack)
}

func (a *ServerWithRoles) GetAlertAcks(ctx context.Context) ([]types.AlertAcknowledgement, error) {
// alert acknowledgement is admin-only for now as it is a fairly niche feature,
// but we may want to develop custom rbac for this feature in the future
// if use of cluster alerts becomes more widespread.
if !a.hasBuiltinRole(types.RoleAdmin) {
return nil, trace.AccessDenied("listing alert acks is admin-only")
// we treat alert acks as an extension of the cluster alert resource rather than its own resource.
if err := a.action(apidefaults.Namespace, types.KindClusterAlert, types.VerbRead, types.VerbList); err != nil {
return nil, trace.Wrap(err)
}

return a.authServer.GetAlertAcks(ctx)
}

func (a *ServerWithRoles) ClearAlertAcks(ctx context.Context, req proto.ClearAlertAcksRequest) error {
// alert acknowledgement is admin-only for now as it is a fairly niche feature,
// but we may want to develop custom rbac for this feature in the future
// if use of cluster alerts becomes more widespread.
if !a.hasBuiltinRole(types.RoleAdmin) {
return trace.AccessDenied("clearing alert acks is admin-only")
// we treat alert acks as an extension of the cluster alert resource rather than its own resource
if err := a.action(apidefaults.Namespace, types.KindClusterAlert, types.VerbDelete); err != nil {
return trace.Wrap(err)
}

return a.authServer.ClearAlertAcks(ctx, req)
Expand Down
43 changes: 35 additions & 8 deletions lib/auth/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3280,23 +3280,40 @@ func TestClusterAlertAccessControls(t *testing.T) {
err = adminClt.UpsertClusterAlert(ctx, alert2)
require.NoError(t, err)

// verify that admin client can see all alerts
alerts, err := adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
// verify that admin client can see all alerts due to resource-level permissions
alerts, err := adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
WithUntargeted: true,
})
require.NoError(t, err)
require.Len(t, alerts, 2)
expectAlerts(alerts, "alert-1", "alert-2")

// verify that WithUntargeted=false admin only observes the alert that specifies
// that it should be shown to all
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
WithUntargeted: false,
})
require.NoError(t, err)
require.Len(t, alerts, 1)
expectAlerts(alerts, "alert-2")

// verify that some other client with no alert-specific permissions can
// see the "permit-all" subset of alerts (using role node here, but any
// role with no special provisions for alerts should be equivalent)
otherClt, err := tt.server.NewClient(TestBuiltin(types.RoleNode))
require.NoError(t, err)
defer otherClt.Close()

alerts, err = otherClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
require.NoError(t, err)
require.Len(t, alerts, 1)
expectAlerts(alerts, "alert-2")
// untargeted and targeted should result in the same behavior since otherClt
// does not have resource-level permissions for the cluster_alert type.
for _, untargeted := range []bool{true, false} {
alerts, err = otherClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
WithUntargeted: untargeted,
})
require.NoError(t, err)
require.Len(t, alerts, 1)
expectAlerts(alerts, "alert-2")
}

// verify that we still reject unauthenticated clients
nopClt, err := tt.server.NewClient(TestBuiltin(types.RoleNop))
Expand Down Expand Up @@ -3327,12 +3344,22 @@ func TestClusterAlertAccessControls(t *testing.T) {
err = adminClt.UpsertClusterAlert(ctx, alert4)
require.NoError(t, err)

// verify that admin client can see all alerts
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
// verify that admin client can see all alerts in untargeted read mode
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
WithUntargeted: true,
})
require.NoError(t, err)
require.Len(t, alerts, 4)
expectAlerts(alerts, "alert-1", "alert-2", "alert-3", "alert-4")

// verify that admin client can see all targeted alerts in targeted mode
alerts, err = adminClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
WithUntargeted: false,
})
require.NoError(t, err)
require.Len(t, alerts, 3)
expectAlerts(alerts, "alert-2", "alert-3", "alert-4")

// verify that node client can only see one of the two new alerts
alerts, err = otherClt.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{})
require.NoError(t, err)
Expand Down
2 changes: 2 additions & 0 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func NewPresetEditorRole() types.Role {
types.NewRule(types.KindLock, RW()),
types.NewRule(types.KindIntegration, append(RW(), types.VerbUse)),
types.NewRule(types.KindBilling, RW()),
types.NewRule(types.KindClusterAlert, RW()),
// Please see defaultAllowRules when adding a new rule.
},
},
Expand Down Expand Up @@ -181,6 +182,7 @@ func NewPresetAuditorRole() types.Role {
types.NewRule(types.KindSession, RO()),
types.NewRule(types.KindEvent, RO()),
types.NewRule(types.KindSessionTracker, RO()),
types.NewRule(types.KindClusterAlert, RO()),
// Please see defaultAllowRules when adding a new rule.
},
},
Expand Down
1 change: 1 addition & 0 deletions tool/tctl/common/alert_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ func (c *AlertCommand) List(ctx context.Context, client auth.ClientI) error {
alerts, err := client.GetClusterAlerts(ctx, types.GetClusterAlertsRequest{
Labels: labels,
WithAcknowledged: c.verbose,
WithUntargeted: true, // include alerts not specifically targeted toward this user
})
if err != nil {
return trace.Wrap(err)
Expand Down