Add the notion of friendly names to access request details. (#27495)

* At the notion of friendly names to access request details.

Access request details can now return friendly names. This will help display
access request resources in a way that allows easier reviewing of Okta apps
and user groups, where the names are not human readable.

* Use friendly name for nodes, tune variable names.

* Add in DetailsID helper function for use by the enterprise UI.

* Request apps instead of app servers, remove DetailsID as its no longer needed.

* Push friendly name into app and usergroup UI objects.

* Fix apps test.

* Linting.

* [Connect] Add FriendlyName to AccessRequest (#27694)

---------

Co-authored-by: Michael <michael.myers@goteleport.com>
This commit is contained in:
Michael Wilson
2023-06-13 14:02:16 +00:00
committed by GitHub
co-authored by Michael
parent 3433ad7f67
commit 1b3201a97c
18 changed files with 324 additions and 47 deletions
+2 -1
View File
@@ -59,7 +59,8 @@ type Resource interface {
// ResourceDetails includes details about the resource
type ResourceDetails struct {
Hostname string
Hostname string
FriendlyName string
}
// ResourceWithSecrets includes additional properties which must
@@ -343,7 +343,8 @@ type ResourceDetails struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
Hostname string `protobuf:"bytes,1,opt,name=hostname,proto3" json:"hostname,omitempty"`
FriendlyName string `protobuf:"bytes,2,opt,name=friendly_name,json=friendlyName,proto3" json:"friendly_name,omitempty"`
}
func (x *ResourceDetails) Reset() {
@@ -385,6 +386,13 @@ func (x *ResourceDetails) GetHostname() string {
return ""
}
func (x *ResourceDetails) GetFriendlyName() string {
if x != nil {
return x.FriendlyName
}
return ""
}
type Resource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -505,24 +513,26 @@ var file_teleport_lib_teleterm_v1_access_request_proto_rawDesc = []byte{
0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x75, 0x62, 0x5f,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x22, 0x2d, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x4e, 0x61, 0x6d, 0x65, 0x22, 0x52, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
0x61, 0x6d, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x12, 0x34, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x74,
0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x72, 0x69, 0x65, 0x6e, 0x64, 0x6c, 0x79, 0x5f,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x66, 0x72, 0x69, 0x65,
0x6e, 0x64, 0x6c, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x85, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x24, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62,
0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x07, 0x64,
0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74,
0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c,
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f,
0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e,
0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x65, 0x74, 0x61, 0x69,
0x6c, 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x54, 0x5a, 0x52, 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, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65,
0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x74,
0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x76,
0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73,
0x42, 0x54, 0x5a, 0x52, 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, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x6c, 0x69, 0x62, 0x2f,
0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x3b, 0x74, 0x65, 0x6c, 0x65,
0x74, 0x65, 0x72, 0x6d, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
+4
View File
@@ -173,6 +173,9 @@ export class ResourceDetails extends jspb.Message {
getHostname(): string;
setHostname(value: string): ResourceDetails;
getFriendlyName(): string;
setFriendlyName(value: string): ResourceDetails;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ResourceDetails.AsObject;
@@ -187,6 +190,7 @@ export class ResourceDetails extends jspb.Message {
export namespace ResourceDetails {
export type AsObject = {
hostname: string,
friendlyName: string,
}
}
+31 -1
View File
@@ -1341,7 +1341,8 @@ proto.teleport.lib.teleterm.v1.ResourceDetails.prototype.toObject = function(opt
*/
proto.teleport.lib.teleterm.v1.ResourceDetails.toObject = function(includeInstance, msg) {
var f, obj = {
hostname: jspb.Message.getFieldWithDefault(msg, 1, "")
hostname: jspb.Message.getFieldWithDefault(msg, 1, ""),
friendlyName: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {
@@ -1382,6 +1383,10 @@ proto.teleport.lib.teleterm.v1.ResourceDetails.deserializeBinaryFromReader = fun
var value = /** @type {string} */ (reader.readString());
msg.setHostname(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.setFriendlyName(value);
break;
default:
reader.skipField();
break;
@@ -1418,6 +1423,13 @@ proto.teleport.lib.teleterm.v1.ResourceDetails.serializeBinaryToWriter = functio
f
);
}
f = message.getFriendlyName();
if (f.length > 0) {
writer.writeString(
2,
f
);
}
};
@@ -1439,6 +1451,24 @@ proto.teleport.lib.teleterm.v1.ResourceDetails.prototype.setHostname = function(
};
/**
* optional string friendly_name = 2;
* @return {string}
*/
proto.teleport.lib.teleterm.v1.ResourceDetails.prototype.getFriendlyName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, ""));
};
/**
* @param {string} value
* @return {!proto.teleport.lib.teleterm.v1.ResourceDetails} returns this
*/
proto.teleport.lib.teleterm.v1.ResourceDetails.prototype.setFriendlyName = function(value) {
return jspb.Message.setProto3StringField(this, 2, value);
};
+15 -14
View File
@@ -1668,14 +1668,14 @@ func (m *RequestValidator) roleAllowsResource(
type ListResourcesRequestOption func(*proto.ListResourcesRequest)
func GetResourceDetails(ctx context.Context, clusterName string, lister ResourceLister, ids []types.ResourceID) (map[string]types.ResourceDetails, error) {
var nodeIDs []types.ResourceID
var resourceIDs []types.ResourceID
for _, resourceID := range ids {
if resourceID.Kind != types.KindNode {
// The only detail we want, for now, is the server hostname, so we
// can skip all other resource kinds as a minor optimization.
continue
// We're interested in hostname or friendly name details. These apply to
// nodes, app servers, and user groups.
switch resourceID.Kind {
case types.KindNode, types.KindApp, types.KindUserGroup:
resourceIDs = append(resourceIDs, resourceID)
}
nodeIDs = append(nodeIDs, resourceID)
}
withExtraRoles := func(req *proto.ListResourcesRequest) {
@@ -1683,36 +1683,37 @@ func GetResourceDetails(ctx context.Context, clusterName string, lister Resource
req.UsePreviewAsRoles = true
}
resources, err := GetResourcesByResourceIDs(ctx, lister, nodeIDs, withExtraRoles)
resources, err := GetResourcesByResourceIDs(ctx, lister, resourceIDs, withExtraRoles)
if err != nil {
return nil, trace.Wrap(err)
}
result := make(map[string]types.ResourceDetails)
for _, resource := range resources {
hn, ok := resource.(interface{ GetHostname() string })
if !ok {
friendlyName := FriendlyName(resource)
// No friendly name was found, so skip to the next resource.
if friendlyName == "" {
continue
}
id := types.ResourceID{
ClusterName: clusterName,
Kind: resource.GetKind(),
Name: resource.GetName(),
}
result[types.ResourceIDToString(id)] = types.ResourceDetails{
Hostname: hn.GetHostname(),
FriendlyName: friendlyName,
}
}
return result, nil
}
func GetNodeResourceIDsByCluster(r types.AccessRequest) map[string][]types.ResourceID {
// GetResourceIDsByCluster will return resource IDs grouped by cluster.
func GetResourceIDsByCluster(r types.AccessRequest) map[string][]types.ResourceID {
resourceIDsByCluster := make(map[string][]types.ResourceID)
for _, resourceID := range r.GetRequestedResourceIDs() {
if resourceID.Kind != types.KindNode {
continue
}
resourceIDsByCluster[resourceID.ClusterName] = append(resourceIDsByCluster[resourceID.ClusterName], resourceID)
}
return resourceIDsByCluster
+97
View File
@@ -1816,3 +1816,100 @@ func TestValidateAccessRequestClusterNames(t *testing.T) {
})
}
}
type mockResourceLister struct {
resources []types.ResourceWithLabels
}
func (m *mockResourceLister) ListResources(ctx context.Context, _ proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
return &types.ListResourcesResponse{
Resources: m.resources,
}, nil
}
func TestGetResourceDetails(t *testing.T) {
clusterName := "cluster"
presence := &mockResourceLister{
resources: []types.ResourceWithLabels{
newNode(t, "node1", "hostname 1"),
newApp(t, "app1", "friendly app 1", types.OriginDynamic),
newApp(t, "app2", "friendly app 2", types.OriginDynamic),
newApp(t, "app3", "friendly app 3", types.OriginOkta),
newUserGroup(t, "group1", "friendly group 1", types.OriginOkta),
},
}
resourceIDs := []types.ResourceID{
newResourceID(clusterName, types.KindNode, "node1"),
newResourceID(clusterName, types.KindApp, "app1"),
newResourceID(clusterName, types.KindApp, "app2"),
newResourceID(clusterName, types.KindApp, "app3"),
newResourceID(clusterName, types.KindUserGroup, "group1"),
}
ctx := context.Background()
details, err := GetResourceDetails(ctx, clusterName, presence, resourceIDs)
require.NoError(t, err)
// Check the resource details to see if friendly names properly propagated.
// Node should be named for its hostname.
require.Equal(t, "hostname 1", details[types.ResourceIDToString(resourceIDs[0])].FriendlyName)
// app1 and app2 are expected to be empty because they're not Okta sourced resources.
require.Empty(t, details[types.ResourceIDToString(resourceIDs[1])].FriendlyName)
require.Empty(t, details[types.ResourceIDToString(resourceIDs[2])].FriendlyName)
// This Okta sourced app should have a friendly name.
require.Equal(t, "friendly app 3", details[types.ResourceIDToString(resourceIDs[3])].FriendlyName)
// This Okta sourced user group should have a friendly name.
require.Equal(t, "friendly group 1", details[types.ResourceIDToString(resourceIDs[4])].FriendlyName)
}
func newNode(t *testing.T, name, hostname string) types.Server {
node, err := types.NewServer(name, types.KindNode,
types.ServerSpecV2{
Hostname: hostname,
})
require.NoError(t, err)
return node
}
func newApp(t *testing.T, name, description, origin string) types.Application {
app, err := types.NewAppV3(types.Metadata{
Name: name,
Description: description,
Labels: map[string]string{
types.OriginLabel: origin,
},
},
types.AppSpecV3{
URI: "https://some-addr.com",
PublicAddr: "https://some-addr.com",
})
require.NoError(t, err)
return app
}
func newUserGroup(t *testing.T, name, description, origin string) types.UserGroup {
userGroup, err := types.NewUserGroup(types.Metadata{
Name: name,
Description: description,
Labels: map[string]string{
types.OriginLabel: origin,
},
})
require.NoError(t, err)
return userGroup
}
func newResourceID(clusterName, kind, name string) types.ResourceID {
return types.ResourceID{
ClusterName: clusterName,
Kind: kind,
Name: name,
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
Copyright 2023 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 services
import "github.com/gravitational/teleport/api/types"
// FriendlyName will return the friendly name for a resource if it has one. Otherwise, it
// will return an empty string.
func FriendlyName(resource types.ResourceWithLabels) string {
// Right now, only resources sourced from Okta and nodes have friendly names.
if resource.Origin() == types.OriginOkta {
return resource.GetMetadata().Description
}
if hn, ok := resource.(interface{ GetHostname() string }); ok {
return hn.GetHostname()
}
return ""
}
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2023 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 services
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
)
func TestFriendlyName(t *testing.T) {
appNoFriendly, err := types.NewAppV3(types.Metadata{
Name: "no friendly",
}, types.AppSpecV3{
URI: "https://some-uri.com",
},
)
require.NoError(t, err)
appFriendly, err := types.NewAppV3(types.Metadata{
Name: "no friendly",
Description: "friendly name",
Labels: map[string]string{
types.OriginLabel: types.OriginOkta,
},
}, types.AppSpecV3{
URI: "https://some-uri.com",
},
)
require.NoError(t, err)
node, err := types.NewServer("node", types.KindNode, types.ServerSpecV2{
Hostname: "friendly hostname",
})
require.NoError(t, err)
tests := []struct {
name string
resource types.ResourceWithLabels
expected string
}{
{
name: "no friendly name",
resource: appNoFriendly,
expected: "",
},
{
name: "friendly app name",
resource: appFriendly,
expected: "friendly name",
},
{
name: "friendly node name",
resource: node,
expected: "friendly hostname",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.expected, FriendlyName(test.resource))
})
}
}
@@ -179,6 +179,7 @@ func resourceIDToString(id *api.ResourceID) string {
func newAPIResourceDetails(details clusters.ResourceDetails) *api.ResourceDetails {
return &api.ResourceDetails{
Hostname: details.Hostname,
Hostname: details.Hostname,
FriendlyName: details.FriendlyName,
}
}
@@ -30,7 +30,8 @@ import (
)
type ResourceDetails struct {
Hostname string
Hostname string
FriendlyName string
}
type AccessRequest struct {
@@ -269,7 +270,7 @@ func (c *Cluster) AssumeRole(ctx context.Context, req *api.AssumeRoleRequest) er
}
func getResourceDetails(ctx context.Context, req types.AccessRequest, clt auth.ClientI) (map[string]ResourceDetails, error) {
resourceIDsByCluster := services.GetNodeResourceIDsByCluster(req)
resourceIDsByCluster := services.GetResourceIDsByCluster(req)
resourceDetails := make(map[string]ResourceDetails)
for clusterName, resourceIDs := range resourceIDsByCluster {
@@ -279,7 +280,7 @@ func getResourceDetails(ctx context.Context, req types.AccessRequest, clt auth.C
}
for id, d := range details {
resourceDetails[id] = ResourceDetails{
Hostname: d.Hostname,
FriendlyName: d.FriendlyName,
}
}
}
+12 -8
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils/aws"
)
@@ -44,6 +45,8 @@ type App struct {
AWSConsole bool `json:"awsConsole"`
// AWSRoles is a list of AWS IAM roles for the application representing AWS console.
AWSRoles []aws.Role `json:"awsRoles,omitempty"`
// FriendlyName is a friendly name for the app.
FriendlyName string `json:"friendlyName,omitempty"`
}
// MakeAppsConfig contains parameters for converting apps to UI representation.
@@ -68,14 +71,15 @@ func MakeApps(c MakeAppsConfig) []App {
labels := makeLabels(teleApp.GetAllLabels())
app := App{
Name: teleApp.GetName(),
Description: teleApp.GetDescription(),
URI: teleApp.GetURI(),
PublicAddr: teleApp.GetPublicAddr(),
Labels: labels,
ClusterID: c.AppClusterName,
FQDN: fqdn,
AWSConsole: teleApp.IsAWSConsole(),
Name: teleApp.GetName(),
Description: teleApp.GetDescription(),
URI: teleApp.GetURI(),
PublicAddr: teleApp.GetPublicAddr(),
Labels: labels,
ClusterID: c.AppClusterName,
FQDN: fqdn,
AWSConsole: teleApp.IsAWSConsole(),
FriendlyName: services.FriendlyName(teleApp),
}
if teleApp.IsAWSConsole() {
+6 -3
View File
@@ -29,6 +29,8 @@ type UserGroup struct {
Description string `json:"description"`
// Labels is the user group list of labels
Labels []Label `json:"labels"`
// FriendlyName is a friendly name for the user group.
FriendlyName string `json:"friendlyName,omitempty"`
}
// MakeUserGroups creates user group objects for the UI.
@@ -38,9 +40,10 @@ func MakeUserGroups(clusterName string, userGroups []types.UserGroup, userRoles
uiLabels := makeLabels(userGroup.GetStaticLabels())
uiUserGroups = append(uiUserGroups, UserGroup{
Name: userGroup.GetName(),
Description: userGroup.GetMetadata().Description,
Labels: uiLabels,
Name: userGroup.GetName(),
Description: userGroup.GetMetadata().Description,
Labels: uiLabels,
FriendlyName: services.FriendlyName(userGroup),
})
}
@@ -63,6 +63,7 @@ message ResourceID {
message ResourceDetails {
string hostname = 1;
string friendly_name = 2;
}
message Resource {
@@ -34,6 +34,7 @@ test('correct formatting of apps fetch response', async () => {
labels: [{ name: 'env', value: 'dev' }],
clusterId: 'cluster-id',
fqdn: 'app-name.example.com',
friendlyName: '',
launchUrl:
'/web/launch/app-name.example.com/cluster-id/app-name.example.com',
awsRoles: [],
@@ -50,6 +51,7 @@ test('correct formatting of apps fetch response', async () => {
labels: [],
clusterId: 'cluster-id',
fqdn: '',
friendlyName: '',
launchUrl: '',
awsRoles: [],
awsConsole: false,
@@ -65,6 +67,7 @@ test('correct formatting of apps fetch response', async () => {
labels: [],
clusterId: 'cluster-id',
fqdn: '',
friendlyName: '',
launchUrl: '',
awsRoles: [],
awsConsole: false,
@@ -28,6 +28,7 @@ export default function makeApp(json: any): App {
clusterId = '',
fqdn = '',
awsConsole = false,
friendlyName = '',
} = json;
const canCreateUrl = fqdn && clusterId && publicAddr;
@@ -66,5 +67,6 @@ export default function makeApp(json: any): App {
awsConsole,
isCloudOrTcpEndpoint: isTcp || isCloud,
addrWithProtocol,
friendlyName,
};
}
@@ -32,6 +32,7 @@ export interface App {
// addrWithProtocol can either be a public address or
// if public address wasn't defined, fallback to uri
addrWithProtocol?: string;
friendlyName?: string;
}
export type AwsRole = {
@@ -17,7 +17,7 @@
import type { UserGroup } from './types';
export function makeUserGroup(json): UserGroup {
const { name, description } = json;
const { name, description, friendlyName } = json;
const labels = json.labels || [];
@@ -25,5 +25,6 @@ export function makeUserGroup(json): UserGroup {
name,
description,
labels,
friendlyName,
};
}
@@ -24,6 +24,8 @@ export type UserGroup = {
description: string;
// Labels for the user group.
labels: AgentLabel[];
// FriendlyName for the user group.
friendlyName?: string;
};
export type UserGroupsResponse = {