Back to Teleport

RFD 0148 - Pinned Unified Resources in the web UI

rfd/0148-pinned-unified-resources.md

19.0.1-dev7.3 KB
Original Source

authors: Michael Myers ([email protected]) state: partially implemented (phase 1 - Teleport 14.1)

RFD 0148 - Pinned Unified Resources in the web UI

Required approvers

engineering: @rosstimothy || @zmb3

What

This RFD discusses the method of "pinning" resources in the web UI. Pinning will allow users to have specific resources in a tab that they want to have easier access to. Pinning is analogous to "favoriting"

Why

Generally, most users access some resources much more than others. We reduced friction of search/discovery by adding in the unified resource view. Pinning takes this view one step further by allowing them to keep their favorite resources always within a click or two away without needing to search and filter for the same resource every day.

Details

User Preferences

Pinned resources will be stored in the ClusterPreferences object as PinnedResources, an array of resource IDs (that match the name sortKey used in our unified resources caches, ex: theservername/node, grafana/app, etc).

diff
// UserPreferencesResponse is the JSON response for the user preferences.
type UserPreferencesResponse struct {
	Assist                     AssistUserPreferencesResponse      `json:"assist"`
	Theme                      userpreferencesv1.Theme            `json:"theme"`
	Onboard                    OnboardUserPreferencesResponse     `json:"onboard"`
+	UnifiedResourcePreferences UnifiedResourcePreferencesResponse `json:"unifiedResourcePreferences"`
+	ClusterPreferences         ClusterUserPreferencesResponse     `json:"clusterPreferences,omitempty"`
}

Defined in protobuf as below:

protobuf
// PinnedResourcesUserPreferences is a collection of resource IDs that will be
// displayed in the user's pinned resources tab in the Web UI.
message PinnedResourcesUserPreferences {
  // resource_ids is a list of unified resource name sort keys.
  repeated string resource_ids = 1;
}

// ClusterUserPreferences are user preferences saved per cluster.
message ClusterUserPreferences {
  // pinned_resources is a list of pinned resources.
  PinnedResourcesUserPreferences pinned_resources = 1;
}

User preferences are generally fetched from the root cluster but apply across both root and leaf clusters. For example, if the user preferences specify that the user prefers the light theme, then we always use the light theme, even if the user switches to a leaf cluster in the UI.

The new ClusterPreferences field contains settings that should be stored and applied per-cluster. Pinned resources are the first such example.

There are two new web API endpoints to retrieve only the cluster preferences. We use the same backend storage as the user preferences because these fields are still user preferences, they are just scoped per-cluster. The other fields of the UserPreferences object are "global" in that they are always fetched from the root server.

go
	// Fetches the user's cluster preferences.
	h.GET("/webapi/user/preferences/:site", h.WithClusterAuth(h.getUserClusterPreferences))

	// Updates the user's cluster preferences.
	h.PUT("/webapi/user/preferences/:site", h.WithClusterAuth(h.updateUserClusterPreferences))

Filtering Pinned Resources

You can think of Pinned Resources as just another filter. However, rather than ascending the tree and populating a list until a limit is met based on RBAC/filters, we individually get each resource by the id passed in ids. This means that the returned list is NOT sorted (because we are fetching in the order the resource was pinned) and must be sorted manually after.

go
// GetUnifiedResourcesByIDs will take a list of ids and return any items found in the unifiedResourceCache tree by id and that return true from matchFn
func (c *UnifiedResourceCache) GetUnifiedResourcesByIDs(ctx context.Context, ids []string, matchFn func(types.ResourceWithLabels) bool) ([]types.ResourceWithLabels, error) {
	var resources []types.ResourceWithLabels

	err := c.read(ctx, func(cache *UnifiedResourceCache) error {
		for _, id := range ids {
			key := backend.Key(prefix, id)
			res, found := cache.nameTree.Get(&item{Key: key})
			if !found || res == nil {
				continue
			}
			resource := cache.resources[res.Value]
			if matched := matchFn(resource); matched {
				resources = append(resources, resource.CloneResource())
			}
		}
		return nil
	})
	if err != nil {
		return nil, trace.Wrap(err, "getting unified resources by id")
	}

	return resources, nil
}

Space limitations for UserPreferences storage

[!NOTE] The maximum item size in DynamoDB is 400 KB, which includes both attribute name binary length (UTF-8 length) and attribute value lengths (again binary length). The attribute name counts towards the size limit.

If we assume an average resource ID is something like db-name-1aaa8584-0e54-4c89-bec9-34f957512078, then we can store well above 10,000 pinned resources per user. This is a very unlikely scenario as any amount of pinned resources over 20, lets just say for conversation sake, defeats the purpose of pinning a resource in the first place. We don't expect anyone to pin more than a "page" worth. We can still limit the resources in the backend to a total (per cluster) of something like 500. These are knobs we can easily turn if necessary but it seems unlikely to "deliberately" go over this cap.

"What happens if a resource I have pinned becomes unavailable?"

In phase 1 (released with Teleport 14.1) if the user preferences contains pinned resource ID that is no longer available, then that resource will not be included in the API response and will not be rendered on-screen. If this resource comes back online, it will start to show up again automatically.

This is acceptable for now, but poses a long-term problem. The reference to the pinned resource ID cannot be deleted since the card is never rendered in the UI and the user doesn't see an "unpin" button.

Phase 2 (approved but not yet implemented) will address this issue. In phase 2, we will return an additional field of 'not found' resource IDs. This will allow the client to render the resource in a "disconnected" state, which will allow the user to decide whether to:

  • keep the pin (because they expect the resource will come back online) or
  • remove the pin (because the resource has been decommissioned).

Security Concerns

Pinned resources go through the same RBAC as unified resources so no additional security concerns matter in the listing.

Backward compatibility

If the user tries to access a cluster that doesn't have access to pinned resources, we can hide the feature and show the normal unified resource view without pinning capability (the same as v14.0 view). This mechanism will be similar to the one used to check if unified resources is enabled by making a fetch to see if the endpoint exists. This can be done per cluster as well since we will be fetching each time the cluster is changed.