diff --git a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go index 18d5ebe1ae0b2..d6a26b87813ce 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/app.pb.go @@ -100,9 +100,14 @@ type App struct { // Only applicable to TCP App Access. // If this field is not empty, URI is expected to contain no port number and start with the tcp // protocol. - TcpPorts []*PortRange `protobuf:"bytes,12,rep,name=tcp_ports,json=tcpPorts,proto3" json:"tcp_ports,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + TcpPorts []*PortRange `protobuf:"bytes,12,rep,name=tcp_ports,json=tcpPorts,proto3" json:"tcp_ports,omitempty"` + // Subkind of the app resource. Used to differentiate different flavors of app. + SubKind string `protobuf:"bytes,13,opt,name=sub_kind,json=subKind,proto3" json:"sub_kind,omitempty"` + // Defines a permission set that is available on an IdentityCenter account app. + // Such apps can be recognized by sub_kind == 'aws_ic_account'. + PermissionSets []*IdentityCenterPermissionSet `protobuf:"bytes,14,rep,name=permission_sets,json=permissionSets,proto3" json:"permission_sets,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *App) Reset() { @@ -219,6 +224,20 @@ func (x *App) GetTcpPorts() []*PortRange { return nil } +func (x *App) GetSubKind() string { + if x != nil { + return x.SubKind + } + return "" +} + +func (x *App) GetPermissionSets() []*IdentityCenterPermissionSet { + if x != nil { + return x.PermissionSets + } + return nil +} + // AwsRole describes AWS IAM role. type AWSRole struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -439,11 +458,76 @@ func (x *RouteToApp) GetTargetPort() uint32 { return 0 } +// Defines a permission set that is available on an IdentityCenter account app. +type IdentityCenterPermissionSet struct { + state protoimpl.MessageState `protogen:"open.v1"` + // AWS-assigned ARN of the permission set. + Arn string `protobuf:"bytes,1,opt,name=arn,proto3" json:"arn,omitempty"` + // Human-readable name of the permission set. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // ID of the Teleport Account Assignment resource that represents this permission being assigned + // on the enclosing Account. + AssignmentId string `protobuf:"bytes,3,opt,name=assignment_id,json=assignmentId,proto3" json:"assignment_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityCenterPermissionSet) Reset() { + *x = IdentityCenterPermissionSet{} + mi := &file_teleport_lib_teleterm_v1_app_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityCenterPermissionSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityCenterPermissionSet) ProtoMessage() {} + +func (x *IdentityCenterPermissionSet) ProtoReflect() protoreflect.Message { + mi := &file_teleport_lib_teleterm_v1_app_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityCenterPermissionSet.ProtoReflect.Descriptor instead. +func (*IdentityCenterPermissionSet) Descriptor() ([]byte, []int) { + return file_teleport_lib_teleterm_v1_app_proto_rawDescGZIP(), []int{4} +} + +func (x *IdentityCenterPermissionSet) GetArn() string { + if x != nil { + return x.Arn + } + return "" +} + +func (x *IdentityCenterPermissionSet) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *IdentityCenterPermissionSet) GetAssignmentId() string { + if x != nil { + return x.AssignmentId + } + return "" +} + var File_teleport_lib_teleterm_v1_app_proto protoreflect.FileDescriptor const file_teleport_lib_teleterm_v1_app_proto_rawDesc = "" + "\n" + - "\"teleport/lib/teleterm/v1/app.proto\x12\x18teleport.lib.teleterm.v1\x1a$teleport/lib/teleterm/v1/label.proto\"\xb3\x03\n" + + "\"teleport/lib/teleterm/v1/app.proto\x12\x18teleport.lib.teleterm.v1\x1a$teleport/lib/teleterm/v1/label.proto\"\xae\x04\n" + "\x03App\x12\x10\n" + "\x03uri\x18\x01 \x01(\tR\x03uri\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12!\n" + @@ -459,7 +543,9 @@ const file_teleport_lib_teleterm_v1_app_proto_rawDesc = "" + "\x04fqdn\x18\n" + " \x01(\tR\x04fqdn\x12>\n" + "\taws_roles\x18\v \x03(\v2!.teleport.lib.teleterm.v1.AWSRoleR\bawsRoles\x12@\n" + - "\ttcp_ports\x18\f \x03(\v2#.teleport.lib.teleterm.v1.PortRangeR\btcpPorts\"h\n" + + "\ttcp_ports\x18\f \x03(\v2#.teleport.lib.teleterm.v1.PortRangeR\btcpPorts\x12\x19\n" + + "\bsub_kind\x18\r \x01(\tR\asubKind\x12^\n" + + "\x0fpermission_sets\x18\x0e \x03(\v25.teleport.lib.teleterm.v1.IdentityCenterPermissionSetR\x0epermissionSets\"h\n" + "\aAWSRole\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\adisplay\x18\x02 \x01(\tR\adisplay\x12\x10\n" + @@ -477,7 +563,11 @@ const file_teleport_lib_teleterm_v1_app_proto_rawDesc = "" + "\fcluster_name\x18\x03 \x01(\tR\vclusterName\x12\x10\n" + "\x03uri\x18\x04 \x01(\tR\x03uri\x12\x1f\n" + "\vtarget_port\x18\x05 \x01(\rR\n" + - "targetPortBTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" + "targetPort\"h\n" + + "\x1bIdentityCenterPermissionSet\x12\x10\n" + + "\x03arn\x18\x01 \x01(\tR\x03arn\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12#\n" + + "\rassignment_id\x18\x03 \x01(\tR\fassignmentIdBTZRgithub.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1;teletermv1b\x06proto3" var ( file_teleport_lib_teleterm_v1_app_proto_rawDescOnce sync.Once @@ -491,23 +581,25 @@ func file_teleport_lib_teleterm_v1_app_proto_rawDescGZIP() []byte { return file_teleport_lib_teleterm_v1_app_proto_rawDescData } -var file_teleport_lib_teleterm_v1_app_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_teleport_lib_teleterm_v1_app_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_teleport_lib_teleterm_v1_app_proto_goTypes = []any{ - (*App)(nil), // 0: teleport.lib.teleterm.v1.App - (*AWSRole)(nil), // 1: teleport.lib.teleterm.v1.AWSRole - (*PortRange)(nil), // 2: teleport.lib.teleterm.v1.PortRange - (*RouteToApp)(nil), // 3: teleport.lib.teleterm.v1.RouteToApp - (*Label)(nil), // 4: teleport.lib.teleterm.v1.Label + (*App)(nil), // 0: teleport.lib.teleterm.v1.App + (*AWSRole)(nil), // 1: teleport.lib.teleterm.v1.AWSRole + (*PortRange)(nil), // 2: teleport.lib.teleterm.v1.PortRange + (*RouteToApp)(nil), // 3: teleport.lib.teleterm.v1.RouteToApp + (*IdentityCenterPermissionSet)(nil), // 4: teleport.lib.teleterm.v1.IdentityCenterPermissionSet + (*Label)(nil), // 5: teleport.lib.teleterm.v1.Label } var file_teleport_lib_teleterm_v1_app_proto_depIdxs = []int32{ - 4, // 0: teleport.lib.teleterm.v1.App.labels:type_name -> teleport.lib.teleterm.v1.Label + 5, // 0: teleport.lib.teleterm.v1.App.labels:type_name -> teleport.lib.teleterm.v1.Label 1, // 1: teleport.lib.teleterm.v1.App.aws_roles:type_name -> teleport.lib.teleterm.v1.AWSRole 2, // 2: teleport.lib.teleterm.v1.App.tcp_ports:type_name -> teleport.lib.teleterm.v1.PortRange - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 4, // 3: teleport.lib.teleterm.v1.App.permission_sets:type_name -> teleport.lib.teleterm.v1.IdentityCenterPermissionSet + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_teleport_lib_teleterm_v1_app_proto_init() } @@ -522,7 +614,7 @@ func file_teleport_lib_teleterm_v1_app_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_lib_teleterm_v1_app_proto_rawDesc), len(file_teleport_lib_teleterm_v1_app_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts index 91158278b1d2f..3b06d2aa13bb0 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/app_pb.ts @@ -145,6 +145,19 @@ export interface App { * @generated from protobuf field: repeated teleport.lib.teleterm.v1.PortRange tcp_ports = 12; */ tcpPorts: PortRange[]; + /** + * Subkind of the app resource. Used to differentiate different flavors of app. + * + * @generated from protobuf field: string sub_kind = 13; + */ + subKind: string; + /** + * Defines a permission set that is available on an IdentityCenter account app. + * Such apps can be recognized by sub_kind == 'aws_ic_account'. + * + * @generated from protobuf field: repeated teleport.lib.teleterm.v1.IdentityCenterPermissionSet permission_sets = 14; + */ + permissionSets: IdentityCenterPermissionSet[]; } /** * AwsRole describes AWS IAM role. @@ -243,6 +256,32 @@ export interface RouteToApp { */ targetPort: number; } +/** + * Defines a permission set that is available on an IdentityCenter account app. + * + * @generated from protobuf message teleport.lib.teleterm.v1.IdentityCenterPermissionSet + */ +export interface IdentityCenterPermissionSet { + /** + * AWS-assigned ARN of the permission set. + * + * @generated from protobuf field: string arn = 1; + */ + arn: string; + /** + * Human-readable name of the permission set. + * + * @generated from protobuf field: string name = 2; + */ + name: string; + /** + * ID of the Teleport Account Assignment resource that represents this permission being assigned + * on the enclosing Account. + * + * @generated from protobuf field: string assignment_id = 3; + */ + assignmentId: string; +} // @generated message type with reflection information, may provide speed optimized methods class App$Type extends MessageType { constructor() { @@ -258,7 +297,9 @@ class App$Type extends MessageType { { no: 9, name: "labels", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => Label }, { no: 10, name: "fqdn", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 11, name: "aws_roles", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => AWSRole }, - { no: 12, name: "tcp_ports", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => PortRange } + { no: 12, name: "tcp_ports", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => PortRange }, + { no: 13, name: "sub_kind", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 14, name: "permission_sets", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => IdentityCenterPermissionSet } ]); } create(value?: PartialMessage): App { @@ -275,6 +316,8 @@ class App$Type extends MessageType { message.fqdn = ""; message.awsRoles = []; message.tcpPorts = []; + message.subKind = ""; + message.permissionSets = []; if (value !== undefined) reflectionMergePartial(this, message, value); return message; @@ -320,6 +363,12 @@ class App$Type extends MessageType { case /* repeated teleport.lib.teleterm.v1.PortRange tcp_ports */ 12: message.tcpPorts.push(PortRange.internalBinaryRead(reader, reader.uint32(), options)); break; + case /* string sub_kind */ 13: + message.subKind = reader.string(); + break; + case /* repeated teleport.lib.teleterm.v1.IdentityCenterPermissionSet permission_sets */ 14: + message.permissionSets.push(IdentityCenterPermissionSet.internalBinaryRead(reader, reader.uint32(), options)); + break; default: let u = options.readUnknownField; if (u === "throw") @@ -368,6 +417,12 @@ class App$Type extends MessageType { /* repeated teleport.lib.teleterm.v1.PortRange tcp_ports = 12; */ for (let i = 0; i < message.tcpPorts.length; i++) PortRange.internalBinaryWrite(message.tcpPorts[i], writer.tag(12, WireType.LengthDelimited).fork(), options).join(); + /* string sub_kind = 13; */ + if (message.subKind !== "") + writer.tag(13, WireType.LengthDelimited).string(message.subKind); + /* repeated teleport.lib.teleterm.v1.IdentityCenterPermissionSet permission_sets = 14; */ + for (let i = 0; i < message.permissionSets.length; i++) + IdentityCenterPermissionSet.internalBinaryWrite(message.permissionSets[i], writer.tag(14, WireType.LengthDelimited).fork(), options).join(); let u = options.writeUnknownFields; if (u !== false) (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); @@ -583,3 +638,66 @@ class RouteToApp$Type extends MessageType { * @generated MessageType for protobuf message teleport.lib.teleterm.v1.RouteToApp */ export const RouteToApp = new RouteToApp$Type(); +// @generated message type with reflection information, may provide speed optimized methods +class IdentityCenterPermissionSet$Type extends MessageType { + constructor() { + super("teleport.lib.teleterm.v1.IdentityCenterPermissionSet", [ + { no: 1, name: "arn", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 2, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, + { no: 3, name: "assignment_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ } + ]); + } + create(value?: PartialMessage): IdentityCenterPermissionSet { + const message = globalThis.Object.create((this.messagePrototype!)); + message.arn = ""; + message.name = ""; + message.assignmentId = ""; + if (value !== undefined) + reflectionMergePartial(this, message, value); + return message; + } + internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: IdentityCenterPermissionSet): IdentityCenterPermissionSet { + let message = target ?? this.create(), end = reader.pos + length; + while (reader.pos < end) { + let [fieldNo, wireType] = reader.tag(); + switch (fieldNo) { + case /* string arn */ 1: + message.arn = reader.string(); + break; + case /* string name */ 2: + message.name = reader.string(); + break; + case /* string assignment_id */ 3: + message.assignmentId = reader.string(); + break; + default: + let u = options.readUnknownField; + if (u === "throw") + throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`); + let d = reader.skip(wireType); + if (u !== false) + (u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d); + } + } + return message; + } + internalBinaryWrite(message: IdentityCenterPermissionSet, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter { + /* string arn = 1; */ + if (message.arn !== "") + writer.tag(1, WireType.LengthDelimited).string(message.arn); + /* string name = 2; */ + if (message.name !== "") + writer.tag(2, WireType.LengthDelimited).string(message.name); + /* string assignment_id = 3; */ + if (message.assignmentId !== "") + writer.tag(3, WireType.LengthDelimited).string(message.assignmentId); + let u = options.writeUnknownFields; + if (u !== false) + (u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer); + return writer; + } +} +/** + * @generated MessageType for protobuf message teleport.lib.teleterm.v1.IdentityCenterPermissionSet + */ +export const IdentityCenterPermissionSet = new IdentityCenterPermissionSet$Type(); diff --git a/lib/teleterm/apiserver/handler/handler_apps.go b/lib/teleterm/apiserver/handler/handler_apps.go index 4ab5aa85403a0..d0b60bf88903c 100644 --- a/lib/teleterm/apiserver/handler/handler_apps.go +++ b/lib/teleterm/apiserver/handler/handler_apps.go @@ -72,6 +72,7 @@ func newAPIApp(clusterApp clusters.App) *api.App { } apiLabels := makeAPILabels(ui.MakeLabelsWithoutInternalPrefixes(app.GetAllLabels())) + permissionSets := makeAPIPermissionSets(app.GetIdentityCenter().GetPermissionSets()) tcpPorts := make([]*api.PortRange, 0, len(app.GetTCPPorts())) for _, portRange := range app.GetTCPPorts() { @@ -79,18 +80,20 @@ func newAPIApp(clusterApp clusters.App) *api.App { } return &api.App{ - Uri: clusterApp.URI.String(), - EndpointUri: app.GetURI(), - Name: app.GetName(), - Desc: app.GetDescription(), - AwsConsole: app.IsAWSConsole(), - PublicAddr: app.GetPublicAddr(), - Fqdn: clusterApp.FQDN, - AwsRoles: awsRoles, - FriendlyName: types.FriendlyName(app), - SamlApp: false, - Labels: apiLabels, - TcpPorts: tcpPorts, + Uri: clusterApp.URI.String(), + EndpointUri: app.GetURI(), + Name: app.GetName(), + Desc: app.GetDescription(), + AwsConsole: app.IsAWSConsole(), + PublicAddr: app.GetPublicAddr(), + Fqdn: clusterApp.FQDN, + AwsRoles: awsRoles, + FriendlyName: types.FriendlyName(app), + SamlApp: false, + Labels: apiLabels, + TcpPorts: tcpPorts, + SubKind: app.GetSubKind(), + PermissionSets: permissionSets, } } @@ -109,3 +112,18 @@ func newSAMLIdPServiceProviderAPIApp(clusterApp clusters.SAMLIdPServiceProvider) Labels: apiLabels, } } + +func makeAPIPermissionSets(sets []*types.IdentityCenterPermissionSet) []*api.IdentityCenterPermissionSet { + if sets == nil { + return nil + } + apiSets := make([]*api.IdentityCenterPermissionSet, len(sets)) + for i, set := range sets { + apiSets[i] = &api.IdentityCenterPermissionSet{ + Name: set.Name, + Arn: set.ARN, + AssignmentId: set.AssignmentID, + } + } + return apiSets +} diff --git a/proto/teleport/lib/teleterm/v1/app.proto b/proto/teleport/lib/teleterm/v1/app.proto index 1b5184be80bf2..e95deeabf98b4 100644 --- a/proto/teleport/lib/teleterm/v1/app.proto +++ b/proto/teleport/lib/teleterm/v1/app.proto @@ -86,6 +86,11 @@ message App { // If this field is not empty, URI is expected to contain no port number and start with the tcp // protocol. repeated PortRange tcp_ports = 12; + // Subkind of the app resource. Used to differentiate different flavors of app. + string sub_kind = 13; + // Defines a permission set that is available on an IdentityCenter account app. + // Such apps can be recognized by sub_kind == 'aws_ic_account'. + repeated IdentityCenterPermissionSet permission_sets = 14; } // AwsRole describes AWS IAM role. @@ -131,3 +136,14 @@ message RouteToApp { // target_port is the port of a multi-port TCP app that the connection is going to be proxied to. uint32 target_port = 5; } + +// Defines a permission set that is available on an IdentityCenter account app. +message IdentityCenterPermissionSet { + // AWS-assigned ARN of the permission set. + string arn = 1; + // Human-readable name of the permission set. + string name = 2; + // ID of the Teleport Account Assignment resource that represents this permission being assigned + // on the enclosing Account. + string assignment_id = 3; +} diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 3155a44c2ec58..c30d92af5df76 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -274,6 +274,24 @@ async function initializeApp(): Promise { if (rootClusterProxyHostAllowList.has(url.host)) { return true; } + + // AWS IAM IC apps. + // Verify that the host is a direct subdomain of awsapp.com and that it has the expected path. + // https://docs.aws.amazon.com/signin/latest/userguide/sign-in-urls-defined.html#access-portal-url + // This of course allows an attacker to create an app on awsapps.com and open it from Connect. + // TODO(ravicious): Allow tsh to bless arbitrary hosts for opening in the browser. + // https://github.com/gravitational/teleport/issues/62808 + const isAwsIc = + url.host.endsWith('.awsapps.com') && + url.host.split('.').length === 3 && + url.pathname === '/start/'; + // https://docs.aws.amazon.com/govcloud-us/latest/UserGuide/govcloud-sso.html#govcloud-diffs-20 + const isAwsIcUsGov = + url.host === 'start.us-gov-home.awsapps.com' && + url.pathname.startsWith('/directory/'); + if (isAwsIc || isAwsIcUsGov) { + return true; + } } // Open links to documentation and GitHub issues in the external browser. diff --git a/web/packages/teleterm/src/services/tshd/app.ts b/web/packages/teleterm/src/services/tshd/app.ts index c5e09ef1ac374..13ce53cb90075 100644 --- a/web/packages/teleterm/src/services/tshd/app.ts +++ b/web/packages/teleterm/src/services/tshd/app.ts @@ -63,6 +63,21 @@ export function getAwsAppLaunchUrl({ }/${publicAddr}/${encodeURIComponent(arn)}`; } +/** Returns a URL that opens the AWS IAM IC app in the browser. */ +export function getAwsIcLaunchUrl({ + app, + roleName, +}: { + app: App; + roleName: string; +}) { + const { publicAddr, subKind } = app; + if (subKind !== 'aws_ic_account') { + return ''; + } + return `${publicAddr}&role_name=${roleName}`; +} + /** Returns a URL that triggers IdP-initiated SSO for SAML Application. */ export function getSamlAppSsoUrl({ app, diff --git a/web/packages/teleterm/src/services/tshd/testHelpers.ts b/web/packages/teleterm/src/services/tshd/testHelpers.ts index b8f1ca87defaf..0ff009a414d3a 100644 --- a/web/packages/teleterm/src/services/tshd/testHelpers.ts +++ b/web/packages/teleterm/src/services/tshd/testHelpers.ts @@ -85,6 +85,8 @@ export const makeApp = (props: Partial = {}): App => ({ uri: appUri, awsRoles: [], tcpPorts: [], + permissionSets: [], + subKind: '', ...props, }); diff --git a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx index f2fce7cdcca7b..4eaaf4a9b85f3 100644 --- a/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.story.tsx @@ -111,6 +111,10 @@ function Buttons(props: StoryProps) { AWS console + + AWS IC + + Cloud app (GCP) @@ -237,6 +241,26 @@ function AwsConsole() { ); } +function AwsIc() { + return ( + + ); +} + function CloudApp() { return ( ({ + name: ps.name, + arn: ps.name, + display: ps.name, + accountId: props.app.name, + })); + + return ( + + getAwsIcLaunchUrl({ + app: props.app, + roleName: arn, + }) + } + onLaunchUrl={props.onLaunchUrl} + isAwsIdentityCenterApp + /> + ); + } + if (props.app.samlApp) { return ( , diff --git a/web/packages/teleterm/src/ui/Search/actions.tsx b/web/packages/teleterm/src/ui/Search/actions.tsx index 13a89cbfd2b1d..9bd5a6f2160b7 100644 --- a/web/packages/teleterm/src/ui/Search/actions.tsx +++ b/web/packages/teleterm/src/ui/Search/actions.tsx @@ -165,7 +165,34 @@ export function mapToAction( { origin: 'search_bar', }, - { arnForAwsApp: parameter.value } + { arnForAwsAppOrRoleForAwsIc: parameter.value } + ), + }; + } + + if (result.resource.subKind === 'aws_ic_account') { + return { + type: 'parametrized-action', + searchResult: result, + parameter: { + getSuggestions: async () => + result.resource.permissionSets.map(p => ({ + value: p.name, + displayText: p.name, + })), + allowOnlySuggestions: true, + noSuggestionsAvailableMessage: 'No permission sets found.', + placeholder: 'Select Permission Set', + }, + perform: parameter => + connectToApp( + ctx, + launchVnet, + result.resource, + { + origin: 'search_bar', + }, + { arnForAwsAppOrRoleForAwsIc: parameter.value } ), }; } diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts index 95e843367609e..23241bd62cb19 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.test.ts @@ -76,7 +76,7 @@ describe('connectToApp', () => { launchVnet, app, { origin: 'resource_table' }, - { arnForAwsApp: 'foo-arn' } + { arnForAwsAppOrRoleForAwsIc: 'foo-arn' } ); expect(window.open).toHaveBeenCalledWith( 'https://teleport-local.com:3080/web/launch/local-app.example.com/teleport-local/local-app.example.com/foo-arn', @@ -84,6 +84,30 @@ describe('connectToApp', () => { 'noreferrer,noopener' ); }); + + test('aws iam ic', async () => { + jest.spyOn(window, 'open').mockImplementation(); + const appContext = new MockAppContext(); + setTestCluster(appContext); + const app = makeApp({ + subKind: 'aws_ic_account', + publicAddr: + 'https://f-139847a43e.awsapps.com/start/#/console?account_id=12312312312', + }); + + await connectToApp( + appContext, + launchVnet, + app, + { origin: 'resource_table' }, + { arnForAwsAppOrRoleForAwsIc: 'foo-role' } + ); + expect(window.open).toHaveBeenCalledWith( + 'https://f-139847a43e.awsapps.com/start/#/console?account_id=12312312312&role_name=foo-role', + '_blank', + 'noreferrer,noopener' + ); + }); }); test('setting up a gateway for TCP app when VNet is not supported', async () => { diff --git a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts index 8343dca0cc6fe..704825867e712 100644 --- a/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts +++ b/web/packages/teleterm/src/ui/services/workspacesService/documentsService/connectToApp.ts @@ -20,6 +20,7 @@ import { App } from 'gen-proto-ts/teleport/lib/teleterm/v1/app_pb'; import { getAwsAppLaunchUrl, + getAwsIcLaunchUrl, getSamlAppSsoUrl, getWebAppLaunchUrl, isWebApp, @@ -50,7 +51,7 @@ export async function connectToApp( target: App, telemetry: { origin: DocumentOrigin }, options?: { - arnForAwsApp?: string; + arnForAwsAppOrRoleForAwsIc?: string; } ): Promise { const rootClusterUri = routing.ensureRootClusterUri(target.uri); @@ -78,7 +79,20 @@ export async function connectToApp( app: target, rootCluster, cluster, - arn: options.arnForAwsApp, + arn: options.arnForAwsAppOrRoleForAwsIc, + }), + telemetry + ); + return; + } + + if (target.subKind === 'aws_ic_account') { + launchAppInBrowser( + ctx, + target, + getAwsIcLaunchUrl({ + app: target, + roleName: options.arnForAwsAppOrRoleForAwsIc, }), telemetry );