mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
[v18] Add Component Feature Advertisement (#62421)
* types: Add `ComponentFeatures` proto types * feat: Add handling for `ComponentFeatures` to Auth, Proxy, AppServer - Impl `ComponentFeatures` for Auth, Proxy, AppServer - Add helper funcs - Add/update tests - Update WebUI types * docs: Update operator crd manifests, tf docs
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||
// source: teleport/componentfeatures/v1/component_features.proto
|
||||
|
||||
package componentfeaturesv1
|
||||
|
||||
import (
|
||||
fmt "fmt"
|
||||
proto "github.com/gogo/protobuf/proto"
|
||||
io "io"
|
||||
math "math"
|
||||
math_bits "math/bits"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
// ComponentFeatureID is an identifier for a specific feature supported by a Teleport component.
|
||||
type ComponentFeatureID int32
|
||||
|
||||
const (
|
||||
ComponentFeatureID_COMPONENT_FEATURE_ID_UNSPECIFIED ComponentFeatureID = 0
|
||||
// ResourceConstraintsV1 indicates support for Resource Constraints and ResourceAccessIDs, as defined in RFD 228.
|
||||
ComponentFeatureID_COMPONENT_FEATURE_ID_RESOURCE_CONSTRAINTS_V1 ComponentFeatureID = 1
|
||||
)
|
||||
|
||||
var ComponentFeatureID_name = map[int32]string{
|
||||
0: "COMPONENT_FEATURE_ID_UNSPECIFIED",
|
||||
1: "COMPONENT_FEATURE_ID_RESOURCE_CONSTRAINTS_V1",
|
||||
}
|
||||
|
||||
var ComponentFeatureID_value = map[string]int32{
|
||||
"COMPONENT_FEATURE_ID_UNSPECIFIED": 0,
|
||||
"COMPONENT_FEATURE_ID_RESOURCE_CONSTRAINTS_V1": 1,
|
||||
}
|
||||
|
||||
func (x ComponentFeatureID) String() string {
|
||||
return proto.EnumName(ComponentFeatureID_name, int32(x))
|
||||
}
|
||||
|
||||
func (ComponentFeatureID) EnumDescriptor() ([]byte, []int) {
|
||||
return fileDescriptor_a4949687356da643, []int{0}
|
||||
}
|
||||
|
||||
// ComponentFeatures represents a set of features supported by a given Teleport component.
|
||||
type ComponentFeatures struct {
|
||||
// features is a list of supported feature identifiers.
|
||||
Features []ComponentFeatureID `protobuf:"varint,1,rep,packed,name=features,proto3,enum=teleport.componentfeatures.v1.ComponentFeatureID" json:"features,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ComponentFeatures) Reset() { *m = ComponentFeatures{} }
|
||||
func (m *ComponentFeatures) String() string { return proto.CompactTextString(m) }
|
||||
func (*ComponentFeatures) ProtoMessage() {}
|
||||
func (*ComponentFeatures) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_a4949687356da643, []int{0}
|
||||
}
|
||||
func (m *ComponentFeatures) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *ComponentFeatures) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_ComponentFeatures.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalToSizedBuffer(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (m *ComponentFeatures) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ComponentFeatures.Merge(m, src)
|
||||
}
|
||||
func (m *ComponentFeatures) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *ComponentFeatures) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ComponentFeatures.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ComponentFeatures proto.InternalMessageInfo
|
||||
|
||||
func (m *ComponentFeatures) GetFeatures() []ComponentFeatureID {
|
||||
if m != nil {
|
||||
return m.Features
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterEnum("teleport.componentfeatures.v1.ComponentFeatureID", ComponentFeatureID_name, ComponentFeatureID_value)
|
||||
proto.RegisterType((*ComponentFeatures)(nil), "teleport.componentfeatures.v1.ComponentFeatures")
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("teleport/componentfeatures/v1/component_features.proto", fileDescriptor_a4949687356da643)
|
||||
}
|
||||
|
||||
var fileDescriptor_a4949687356da643 = []byte{
|
||||
// 256 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x2b, 0x49, 0xcd, 0x49,
|
||||
0x2d, 0xc8, 0x2f, 0x2a, 0xd1, 0x4f, 0xce, 0xcf, 0x2d, 0xc8, 0xcf, 0x4b, 0xcd, 0x2b, 0x49, 0x4b,
|
||||
0x4d, 0x2c, 0x29, 0x2d, 0x4a, 0x2d, 0xd6, 0x2f, 0x33, 0x44, 0x08, 0xc6, 0xc3, 0x44, 0xf5, 0x0a,
|
||||
0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0x64, 0x61, 0xfa, 0xf4, 0x30, 0xf4, 0xe9, 0x95, 0x19, 0x2a, 0x25,
|
||||
0x71, 0x09, 0x3a, 0xc3, 0xc4, 0xdd, 0xa0, 0xe2, 0x42, 0xbe, 0x5c, 0x1c, 0x30, 0x35, 0x12, 0x8c,
|
||||
0x0a, 0xcc, 0x1a, 0x7c, 0x46, 0x86, 0x7a, 0x78, 0x8d, 0xd1, 0x43, 0x37, 0xc3, 0xd3, 0x25, 0x08,
|
||||
0x6e, 0x84, 0x56, 0x0e, 0x97, 0x10, 0xa6, 0xbc, 0x90, 0x0a, 0x97, 0x82, 0xb3, 0xbf, 0x6f, 0x80,
|
||||
0xbf, 0x9f, 0xab, 0x5f, 0x48, 0xbc, 0x9b, 0xab, 0x63, 0x48, 0x68, 0x90, 0x6b, 0xbc, 0xa7, 0x4b,
|
||||
0x7c, 0xa8, 0x5f, 0x70, 0x80, 0xab, 0xb3, 0xa7, 0x9b, 0xa7, 0xab, 0x8b, 0x00, 0x83, 0x90, 0x01,
|
||||
0x97, 0x0e, 0x56, 0x55, 0x41, 0xae, 0xc1, 0xfe, 0xa1, 0x41, 0xce, 0xae, 0xf1, 0xce, 0xfe, 0x7e,
|
||||
0xc1, 0x21, 0x41, 0x8e, 0x9e, 0x7e, 0x21, 0xc1, 0xf1, 0x61, 0x86, 0x02, 0x8c, 0x4e, 0x45, 0x27,
|
||||
0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0x63, 0x54, 0x4a, 0x7a, 0x66,
|
||||
0x49, 0x46, 0x69, 0x12, 0xc8, 0xd5, 0xfa, 0xe9, 0x45, 0x89, 0x65, 0x99, 0x25, 0x89, 0x25, 0x99,
|
||||
0xf9, 0x79, 0x89, 0x39, 0xfa, 0xf0, 0xf0, 0x4c, 0x2c, 0xc8, 0xd4, 0x4f, 0x4f, 0xcd, 0xd3, 0x07,
|
||||
0x87, 0x97, 0x7e, 0x7a, 0xbe, 0x3e, 0xde, 0x90, 0xb6, 0xc6, 0x10, 0x2c, 0x33, 0x4c, 0x62, 0x03,
|
||||
0xeb, 0x35, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x72, 0x26, 0xf7, 0x8d, 0xa5, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *ComponentFeatures) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalToSizedBuffer(dAtA[:size])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *ComponentFeatures) MarshalTo(dAtA []byte) (int, error) {
|
||||
size := m.Size()
|
||||
return m.MarshalToSizedBuffer(dAtA[:size])
|
||||
}
|
||||
|
||||
func (m *ComponentFeatures) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
i := len(dAtA)
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.XXX_unrecognized != nil {
|
||||
i -= len(m.XXX_unrecognized)
|
||||
copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
if len(m.Features) > 0 {
|
||||
dAtA2 := make([]byte, len(m.Features)*10)
|
||||
var j1 int
|
||||
for _, num := range m.Features {
|
||||
for num >= 1<<7 {
|
||||
dAtA2[j1] = uint8(uint64(num)&0x7f | 0x80)
|
||||
num >>= 7
|
||||
j1++
|
||||
}
|
||||
dAtA2[j1] = uint8(num)
|
||||
j1++
|
||||
}
|
||||
i -= j1
|
||||
copy(dAtA[i:], dAtA2[:j1])
|
||||
i = encodeVarintComponentFeatures(dAtA, i, uint64(j1))
|
||||
i--
|
||||
dAtA[i] = 0xa
|
||||
}
|
||||
return len(dAtA) - i, nil
|
||||
}
|
||||
|
||||
func encodeVarintComponentFeatures(dAtA []byte, offset int, v uint64) int {
|
||||
offset -= sovComponentFeatures(v)
|
||||
base := offset
|
||||
for v >= 1<<7 {
|
||||
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||
v >>= 7
|
||||
offset++
|
||||
}
|
||||
dAtA[offset] = uint8(v)
|
||||
return base
|
||||
}
|
||||
func (m *ComponentFeatures) Size() (n int) {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Features) > 0 {
|
||||
l = 0
|
||||
for _, e := range m.Features {
|
||||
l += sovComponentFeatures(uint64(e))
|
||||
}
|
||||
n += 1 + sovComponentFeatures(uint64(l)) + l
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func sovComponentFeatures(x uint64) (n int) {
|
||||
return (math_bits.Len64(x|1) + 6) / 7
|
||||
}
|
||||
func sozComponentFeatures(x uint64) (n int) {
|
||||
return sovComponentFeatures(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||
}
|
||||
func (m *ComponentFeatures) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= uint64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: ComponentFeatures: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: ComponentFeatures: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType == 0 {
|
||||
var v ComponentFeatureID
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= ComponentFeatureID(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.Features = append(m.Features, v)
|
||||
} else if wireType == 2 {
|
||||
var packedLen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
packedLen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if packedLen < 0 {
|
||||
return ErrInvalidLengthComponentFeatures
|
||||
}
|
||||
postIndex := iNdEx + packedLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthComponentFeatures
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
var elementCount int
|
||||
if elementCount != 0 && len(m.Features) == 0 {
|
||||
m.Features = make([]ComponentFeatureID, 0, elementCount)
|
||||
}
|
||||
for iNdEx < postIndex {
|
||||
var v ComponentFeatureID
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
v |= ComponentFeatureID(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
m.Features = append(m.Features, v)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Features", wireType)
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipComponentFeatures(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if (skippy < 0) || (iNdEx+skippy) < 0 {
|
||||
return ErrInvalidLengthComponentFeatures
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipComponentFeatures(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
depth := 0
|
||||
for iNdEx < l {
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
wireType := int(wire & 0x7)
|
||||
switch wireType {
|
||||
case 0:
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx++
|
||||
if dAtA[iNdEx-1] < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
iNdEx += 8
|
||||
case 2:
|
||||
var length int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowComponentFeatures
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
length |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if length < 0 {
|
||||
return 0, ErrInvalidLengthComponentFeatures
|
||||
}
|
||||
iNdEx += length
|
||||
case 3:
|
||||
depth++
|
||||
case 4:
|
||||
if depth == 0 {
|
||||
return 0, ErrUnexpectedEndOfGroupComponentFeatures
|
||||
}
|
||||
depth--
|
||||
case 5:
|
||||
iNdEx += 4
|
||||
default:
|
||||
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||
}
|
||||
if iNdEx < 0 {
|
||||
return 0, ErrInvalidLengthComponentFeatures
|
||||
}
|
||||
if depth == 0 {
|
||||
return iNdEx, nil
|
||||
}
|
||||
}
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidLengthComponentFeatures = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||
ErrIntOverflowComponentFeatures = fmt.Errorf("proto: integer overflow")
|
||||
ErrUnexpectedEndOfGroupComponentFeatures = fmt.Errorf("proto: unexpected end of group")
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 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.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package teleport.componentfeatures.v1;
|
||||
|
||||
option go_package = "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1;componentfeaturesv1";
|
||||
|
||||
// ComponentFeatureID is an identifier for a specific feature supported by a Teleport component.
|
||||
enum ComponentFeatureID {
|
||||
COMPONENT_FEATURE_ID_UNSPECIFIED = 0;
|
||||
// ResourceConstraintsV1 indicates support for Resource Constraints and ResourceAccessIDs, as defined in RFD 228.
|
||||
COMPONENT_FEATURE_ID_RESOURCE_CONSTRAINTS_V1 = 1;
|
||||
}
|
||||
|
||||
// ComponentFeatures represents a set of features supported by a given Teleport component.
|
||||
message ComponentFeatures {
|
||||
// features is a list of supported feature identifiers.
|
||||
repeated ComponentFeatureID features = 1;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
import "teleport/attestation/v1/attestation.proto";
|
||||
import "teleport/componentfeatures/v1/component_features.proto";
|
||||
import "teleport/legacy/types/trusted_device_requirement.proto";
|
||||
import "teleport/legacy/types/wrappers/wrappers.proto";
|
||||
|
||||
@@ -972,6 +973,9 @@ message ServerSpecV2 {
|
||||
// the list of Relay host IDs that the server is connected to
|
||||
repeated string relay_ids = 17;
|
||||
|
||||
// component_features represents features supported by this server
|
||||
teleport.componentfeatures.v1.ComponentFeatures component_features = 18;
|
||||
|
||||
reserved 8;
|
||||
reserved 10;
|
||||
reserved "KubernetesClusters";
|
||||
@@ -1060,6 +1064,9 @@ message AppServerSpecV3 {
|
||||
string relay_group = 7;
|
||||
// the list of Relay host IDs that the server is connected to
|
||||
repeated string relay_ids = 8;
|
||||
|
||||
// component_features contains features supported by this app server.
|
||||
teleport.componentfeatures.v1.ComponentFeatures component_features = 9;
|
||||
}
|
||||
|
||||
// AppV3List represents a list of app resources.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport/api"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/api/utils/iterutils"
|
||||
)
|
||||
@@ -70,6 +71,10 @@ type AppServer interface {
|
||||
GetRelayIDs() []string
|
||||
// GetScope returns the scope this server belongs to.
|
||||
GetScope() string
|
||||
// GetComponentFeatures returns the ComponentFeatures supported by this AppServer.
|
||||
GetComponentFeatures() *componentfeaturesv1.ComponentFeatures
|
||||
// SetComponentFeatures sets the ComponentFeatures supported by this AppServer.
|
||||
SetComponentFeatures(*componentfeaturesv1.ComponentFeatures)
|
||||
}
|
||||
|
||||
// NewAppServerV3 creates a new app server instance.
|
||||
@@ -114,6 +119,16 @@ func NewAppServerForAWSOIDCIntegration(integrationName, hostID, publicAddr strin
|
||||
})
|
||||
}
|
||||
|
||||
// GetComponentFeatures returns the ComponentFeatures supported by this AppServer.
|
||||
func (s *AppServerV3) GetComponentFeatures() *componentfeaturesv1.ComponentFeatures {
|
||||
return s.Spec.ComponentFeatures
|
||||
}
|
||||
|
||||
// SetComponentFeatures sets the ComponentFeatures supported by this AppServer.
|
||||
func (s *AppServerV3) SetComponentFeatures(cf *componentfeaturesv1.ComponentFeatures) {
|
||||
s.Spec.ComponentFeatures = cf
|
||||
}
|
||||
|
||||
// GetVersion returns the database server resource version.
|
||||
func (s *AppServerV3) GetVersion() string {
|
||||
return s.Version
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/api/utils/aws"
|
||||
)
|
||||
@@ -114,6 +115,11 @@ type Server interface {
|
||||
GetGitHub() *GitHubServerMetadata
|
||||
// GetScope returns the scope this server belongs to.
|
||||
GetScope() string
|
||||
|
||||
// GetComponentFeatures returns the supported features for the server.
|
||||
GetComponentFeatures() *componentfeaturesv1.ComponentFeatures
|
||||
// SetComponentFeatures sets the supported features for the server.
|
||||
SetComponentFeatures(*componentfeaturesv1.ComponentFeatures)
|
||||
}
|
||||
|
||||
// NewServer creates an instance of Server.
|
||||
@@ -195,6 +201,16 @@ func NewGitHubServerWithName(name string, githubSpec GitHubServerMetadata) (Serv
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// GetComponentFeatures returns the supported features for the server.
|
||||
func (s *ServerV2) GetComponentFeatures() *componentfeaturesv1.ComponentFeatures {
|
||||
return s.Spec.ComponentFeatures
|
||||
}
|
||||
|
||||
// SetComponentFeatures sets the supported features for the server.
|
||||
func (s *ServerV2) SetComponentFeatures(features *componentfeaturesv1.ComponentFeatures) {
|
||||
s.Spec.ComponentFeatures = features
|
||||
}
|
||||
|
||||
// GetVersion returns resource version
|
||||
func (s *ServerV2) GetVersion() string {
|
||||
return s.Version
|
||||
|
||||
+2699
-2583
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ inputs:
|
||||
exclude_paths:
|
||||
# generated by buf-gogo.gen.yaml
|
||||
- api/proto/teleport/attestation/
|
||||
- api/proto/teleport/componentfeatures/
|
||||
- api/proto/teleport/legacy/
|
||||
- api/proto/teleport/mfa/
|
||||
- api/proto/teleport/usageevents/
|
||||
|
||||
@@ -4,6 +4,7 @@ inputs:
|
||||
- directory: .
|
||||
paths:
|
||||
- api/proto/teleport/attestation/
|
||||
- api/proto/teleport/componentfeatures/
|
||||
- api/proto/teleport/legacy/
|
||||
- api/proto/teleport/mfa/
|
||||
- api/proto/teleport/usageevents/
|
||||
|
||||
@@ -43,6 +43,8 @@ var ignoredFields = map[string]stringSet{
|
||||
// Useless field for agentless servers, and potentially dangerous as it
|
||||
// allows remote exec on agentful nodes.
|
||||
"CmdLabels": struct{}{},
|
||||
// Not intended for manual usage.
|
||||
"component_features": struct{}{},
|
||||
},
|
||||
"TrustedClusterSpecV2": {
|
||||
"Roles": struct{}{}, // Deprecated, use RoleMap instead.
|
||||
|
||||
@@ -178,6 +178,7 @@ exclude_fields:
|
||||
|
||||
# Server
|
||||
- "ServerSpecV2.CmdLabels"
|
||||
- "ServerSpecV2.component_features"
|
||||
|
||||
# SessionRecordingConfig
|
||||
- "SessionRecordingConfigV2.Metadata.Name" # It's a singleton resource
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
proto "github.com/gogo/protobuf/proto"
|
||||
github_com_gravitational_teleport_api_constants "github.com/gravitational/teleport/api/constants"
|
||||
_ "github.com/gravitational/teleport/api/gen/proto/go/attestation/v1"
|
||||
_ "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
github_com_gravitational_teleport_api_types "github.com/gravitational/teleport/api/types"
|
||||
github_com_hashicorp_terraform_plugin_framework_attr "github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
github_com_hashicorp_terraform_plugin_framework_diag "github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package componentfeatures
|
||||
|
||||
import (
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
// ForAuthServer returns features that an Auth server can support/participate in.
|
||||
func ForAuthServer() *componentfeaturesv1.ComponentFeatures {
|
||||
return New(FeatureResourceConstraintsV1)
|
||||
}
|
||||
|
||||
type sshServerInfoGetter interface {
|
||||
GetProxyMode() bool
|
||||
}
|
||||
|
||||
// ForSSHServer returns features that an SSH/Proxy server can support/participate in.
|
||||
func ForSSHServer(g sshServerInfoGetter) *componentfeaturesv1.ComponentFeatures {
|
||||
// Resource Constraints are only supported for Proxy servers.
|
||||
if !g.GetProxyMode() {
|
||||
return New()
|
||||
}
|
||||
|
||||
return New(FeatureResourceConstraintsV1)
|
||||
}
|
||||
|
||||
type appServerInfoGetter interface {
|
||||
GetApp() types.Application
|
||||
}
|
||||
|
||||
// ForAppServer returns features that an App server can support/participate in.
|
||||
func ForAppServer(g appServerInfoGetter) *componentfeaturesv1.ComponentFeatures {
|
||||
// Resource Constraints are only supported for AWS Console apps.
|
||||
if app := g.GetApp(); !app.IsAWSConsole() {
|
||||
return New()
|
||||
}
|
||||
|
||||
return New(FeatureResourceConstraintsV1)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package componentfeatures
|
||||
|
||||
import (
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
)
|
||||
|
||||
// FeatureID is used to wrap [componentfeaturesv1.ComponentFeatureID] for convenience methods.
|
||||
type FeatureID int32
|
||||
|
||||
const (
|
||||
// FeatureUnspecified is the unspecified feature ID.
|
||||
FeatureUnspecified FeatureID = iota
|
||||
// FeatureResourceConstraintsV1 indicates support for Resource Constraints in Access Requests,
|
||||
// identity certificates, and AWS Console App resources.
|
||||
FeatureResourceConstraintsV1
|
||||
)
|
||||
|
||||
var featureIDToName = map[FeatureID]string{
|
||||
FeatureUnspecified: "UNSPECIFIED",
|
||||
FeatureResourceConstraintsV1: "RESOURCE_CONSTRAINTS_V1",
|
||||
}
|
||||
|
||||
// String returns a short name for the FeatureID, falling back to the
|
||||
// [componentfeaturesv1.ComponentFeatureID] enum name if not specified.
|
||||
func (f FeatureID) String() string {
|
||||
if s, ok := featureIDToName[f]; ok {
|
||||
return s
|
||||
}
|
||||
if s, ok := componentfeaturesv1.ComponentFeatureID_name[int32(f)]; ok {
|
||||
return s
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
// ToProto converts the FeatureID to its corresponding [componentfeaturesv1.ComponentFeatureID].
|
||||
func (f FeatureID) ToProto() componentfeaturesv1.ComponentFeatureID {
|
||||
return componentfeaturesv1.ComponentFeatureID(f)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package componentfeatures
|
||||
|
||||
import componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
|
||||
// New creates a new [componentfeaturesv1.ComponentFeatures] struct containing the provided FeatureIDs.
|
||||
func New(features ...FeatureID) *componentfeaturesv1.ComponentFeatures {
|
||||
out := &componentfeaturesv1.ComponentFeatures{}
|
||||
seen := make(map[FeatureID]struct{})
|
||||
for _, f := range features {
|
||||
if _, exists := seen[f]; exists {
|
||||
continue
|
||||
}
|
||||
seen[f] = struct{}{}
|
||||
out.Features = append(out.Features, f.ToProto())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Join copies, deduplicates, and combines [componentfeaturesv1.ComponentFeatures] sets into a single set
|
||||
// containing all unique features.
|
||||
func Join(sets ...*componentfeaturesv1.ComponentFeatures) *componentfeaturesv1.ComponentFeatures {
|
||||
out := &componentfeaturesv1.ComponentFeatures{}
|
||||
seen := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
||||
|
||||
for _, fs := range sets {
|
||||
if fs == nil || len(fs.GetFeatures()) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, f := range fs.GetFeatures() {
|
||||
if _, exists := seen[f]; !exists {
|
||||
seen[f] = struct{}{}
|
||||
out.Features = append(out.Features, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// InAllSets reports whether a given [componentfeaturesv1.ComponentFeatureID] is
|
||||
// present in *every* [componentfeaturesv1.ComponentFeatures] set.
|
||||
//
|
||||
// If no sets are provided, or any set is nil, it returns false.
|
||||
func InAllSets(feature FeatureID, sets ...*componentfeaturesv1.ComponentFeatures) bool {
|
||||
if len(sets) == 0 {
|
||||
return false
|
||||
}
|
||||
proto := feature.ToProto()
|
||||
|
||||
for _, fs := range sets {
|
||||
if fs == nil || len(fs.GetFeatures()) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, f := range fs.GetFeatures() {
|
||||
if f == proto {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Intersect returns a new [componentfeaturesv1.ComponentFeatures] containing only features present in all input
|
||||
// [componentfeaturesv1.ComponentFeatures].
|
||||
//
|
||||
// If no sets are provided, or any set is nil or empty, it returns an empty [componentfeaturesv1.ComponentFeatures].
|
||||
func Intersect(sets ...*componentfeaturesv1.ComponentFeatures) *componentfeaturesv1.ComponentFeatures {
|
||||
out := &componentfeaturesv1.ComponentFeatures{}
|
||||
|
||||
if len(sets) == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
counts := make(map[componentfeaturesv1.ComponentFeatureID]int)
|
||||
|
||||
for _, fs := range sets {
|
||||
if fs == nil || len(fs.GetFeatures()) == 0 {
|
||||
return out
|
||||
}
|
||||
|
||||
seenInThisSet := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
||||
for _, f := range fs.GetFeatures() {
|
||||
if _, seen := seenInThisSet[f]; seen {
|
||||
continue
|
||||
}
|
||||
seenInThisSet[f] = struct{}{}
|
||||
counts[f]++
|
||||
}
|
||||
}
|
||||
|
||||
for f, c := range counts {
|
||||
if c == len(sets) {
|
||||
out.Features = append(out.Features, f)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ToIntegers deduplicates and converts [componentfeaturesv1.ComponentFeatures] into a list of integers.
|
||||
func ToIntegers(features *componentfeaturesv1.ComponentFeatures) []int {
|
||||
out := make([]int, 0)
|
||||
seen := make(map[componentfeaturesv1.ComponentFeatureID]struct{})
|
||||
|
||||
for _, f := range features.GetFeatures() {
|
||||
if _, seen := seen[f]; seen {
|
||||
continue
|
||||
}
|
||||
seen[f] = struct{}{}
|
||||
out = append(out, int(f))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package componentfeatures
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
featA = FeatureResourceConstraintsV1
|
||||
featB = FeatureID(200)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
features []FeatureID
|
||||
want []componentfeaturesv1.ComponentFeatureID
|
||||
}{
|
||||
{
|
||||
name: "no features yields empty set",
|
||||
features: []FeatureID{},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{},
|
||||
},
|
||||
{
|
||||
name: "duplicates are ignored",
|
||||
features: []FeatureID{featA, featB, featA, featB},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := New(tt.features...)
|
||||
require.NotNil(t, got, "New(%v) returned nil", tt.features)
|
||||
require.ElementsMatch(t, tt.want, got.Features, "New(%v) = %v; want %v", tt.features, got.Features, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
featA = FeatureResourceConstraintsV1
|
||||
featB = FeatureID(200)
|
||||
featC = FeatureID(201)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sets []*componentfeaturesv1.ComponentFeatures
|
||||
want []componentfeaturesv1.ComponentFeatureID
|
||||
}{
|
||||
{
|
||||
name: "no sets returns empty set",
|
||||
sets: nil,
|
||||
want: []componentfeaturesv1.ComponentFeatureID{},
|
||||
},
|
||||
{
|
||||
name: "single set returns same set",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto()},
|
||||
},
|
||||
{
|
||||
name: "multiple sets combined",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA), New(featB), New(featA, featC)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto(), featC.ToProto()},
|
||||
},
|
||||
{
|
||||
name: "nil and empty sets ignored",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA), nil, New(), New(featB)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := Join(tt.sets...)
|
||||
require.NotNil(t, got, "Join(%v) returned nil", tt.sets)
|
||||
require.ElementsMatch(t, tt.want, got.Features, "Join(%v) = %v; want %v", tt.sets, got.Features, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInAllSets(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
featA = FeatureResourceConstraintsV1
|
||||
featB = FeatureID(200)
|
||||
featC = FeatureID(201)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sets []*componentfeaturesv1.ComponentFeatures
|
||||
feature FeatureID
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "no sets",
|
||||
sets: nil,
|
||||
feature: featA,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "single set contains feature",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA)},
|
||||
feature: featA,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "single set missing feature",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featB)},
|
||||
feature: featA,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "multiple sets all contain feature",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB), New(featA), New(featA, featB, featC)},
|
||||
feature: featA,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "one set missing feature",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA), New(featA, featB), New(featB, featC)},
|
||||
feature: featA,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil set treated as unsupported",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featB, featC), nil, New(featA, featB)},
|
||||
feature: featB,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty set treated as unsupported",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA), New(), New(featA, featB)},
|
||||
feature: featA,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "duplicate feature in set",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featA, featB), New(featA, featC, featC)},
|
||||
feature: featA,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := InAllSets(tt.feature, tt.sets...)
|
||||
require.Equal(t, tt.want, got, "FeatureInAllSets(%v, %v) = %v; want %v", tt.feature, tt.sets, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntersect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
featA = FeatureResourceConstraintsV1
|
||||
featB = FeatureID(200)
|
||||
featC = FeatureID(201)
|
||||
featD = FeatureID(202)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sets []*componentfeaturesv1.ComponentFeatures
|
||||
want []componentfeaturesv1.ComponentFeatureID
|
||||
}{
|
||||
{
|
||||
name: "no sets returns empty set",
|
||||
sets: nil,
|
||||
want: []componentfeaturesv1.ComponentFeatureID{},
|
||||
},
|
||||
{
|
||||
name: "nil set returns empty set",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB), nil, New(featA, featC)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{},
|
||||
},
|
||||
{
|
||||
name: "disjoint sets return empty set",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB), New(featC, featD)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{},
|
||||
},
|
||||
{
|
||||
name: "single set returns same set",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto()},
|
||||
},
|
||||
{
|
||||
name: "sets with full intersection",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB, featD), New(featD, featA, featB), New(featB, featD, featA)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto(), featB.ToProto(), featD.ToProto()},
|
||||
},
|
||||
{
|
||||
name: "sets with partial intersection",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB, featC), New(featB, featC, featD), New(featB, featD)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featB.ToProto()},
|
||||
},
|
||||
{
|
||||
name: "duplicate features do not affect intersection",
|
||||
sets: []*componentfeaturesv1.ComponentFeatures{New(featA, featB, featB), New(featA, featA, featB, featC), New(featA, featD)},
|
||||
want: []componentfeaturesv1.ComponentFeatureID{featA.ToProto()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := Intersect(tt.sets...)
|
||||
require.NotNil(t, got, "Intersect should not return nil")
|
||||
|
||||
// Order not guaranteed, so compare as sets.
|
||||
require.ElementsMatch(t, tt.want, got.Features, "Intersect(%v) = %v; want %v", tt.sets, got.Features, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/cloud/imds/azure"
|
||||
gcpimds "github.com/gravitational/teleport/lib/cloud/imds/gcp"
|
||||
oracleimds "github.com/gravitational/teleport/lib/cloud/imds/oracle"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/events/athena"
|
||||
@@ -2774,9 +2775,10 @@ func (process *TeleportProcess) initAuthService() error {
|
||||
Name: connector.HostUUID(),
|
||||
},
|
||||
Spec: types.ServerSpecV2{
|
||||
Addr: authAddr,
|
||||
Hostname: process.Config.Hostname,
|
||||
Version: teleport.Version,
|
||||
Addr: authAddr,
|
||||
Hostname: process.Config.Hostname,
|
||||
Version: teleport.Version,
|
||||
ComponentFeatures: componentfeatures.ForAuthServer(),
|
||||
},
|
||||
}
|
||||
state, err := process.storage.GetState(process.GracefulExitContext(), types.RoleAdmin)
|
||||
|
||||
@@ -33,9 +33,11 @@ import (
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
logutils "github.com/gravitational/teleport/lib/utils/log"
|
||||
)
|
||||
@@ -954,6 +956,14 @@ func newResourceCollection(r resource) resourceCollection {
|
||||
status: aggregateHealthStatuses(servers),
|
||||
}
|
||||
})
|
||||
case types.AppServer:
|
||||
return newServerResourceCollection(r,
|
||||
func(srv types.AppServer, servers map[string]types.AppServer) types.AppServer {
|
||||
return &aggregatedAppServer{
|
||||
AppServer: srv,
|
||||
features: intersectComponentFeaturesForAppServers(servers),
|
||||
}
|
||||
})
|
||||
case serverResource:
|
||||
return newServerResourceCollection(r, nil)
|
||||
default:
|
||||
@@ -1037,6 +1047,44 @@ func (c *serverResourceCollection[R]) remove(r types.Resource) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// aggregatedAppServer wraps an app server with aggregated ComponentFeatures
|
||||
// in order to perform an intersection of all features reported by multiple
|
||||
// AppServers serving the same app. Only ComponentFeatures supported by *all*
|
||||
// AppServers will be reported in the aggregated Resource.
|
||||
type aggregatedAppServer struct {
|
||||
types.AppServer
|
||||
features *componentfeaturesv1.ComponentFeatures
|
||||
}
|
||||
|
||||
// Copy returns a copy of the underlying app server with aggregated
|
||||
// [componentfeaturesv1.ComponentFeatures].
|
||||
func (a *aggregatedAppServer) Copy() types.AppServer {
|
||||
out := a.AppServer.Copy()
|
||||
out.SetComponentFeatures(a.GetComponentFeatures())
|
||||
return out
|
||||
}
|
||||
|
||||
// CloneResource returns a copy of the underlying app server with
|
||||
// aggregated [componentfeaturesv1.ComponentFeatures].
|
||||
func (a *aggregatedAppServer) CloneResource() types.ResourceWithLabels {
|
||||
return a.Copy()
|
||||
}
|
||||
|
||||
func (a *aggregatedAppServer) GetComponentFeatures() *componentfeaturesv1.ComponentFeatures {
|
||||
if a.features == nil {
|
||||
return nil
|
||||
}
|
||||
return componentfeatures.Join(a.features)
|
||||
}
|
||||
|
||||
func intersectComponentFeaturesForAppServers(servers map[string]types.AppServer) *componentfeaturesv1.ComponentFeatures {
|
||||
allFeatures := make([]*componentfeaturesv1.ComponentFeatures, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
allFeatures = append(allFeatures, s.GetComponentFeatures())
|
||||
}
|
||||
return componentfeatures.Intersect(allFeatures...)
|
||||
}
|
||||
|
||||
// aggregatedDatabase wraps a database server with aggregated health status.
|
||||
// It is assumed that multiple heartbeats with the same resource name but
|
||||
// different host IDs may be received and they may report different health
|
||||
|
||||
@@ -37,6 +37,7 @@ import (
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/defaults"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
|
||||
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
@@ -44,6 +45,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types/header"
|
||||
"github.com/gravitational/teleport/lib/backend"
|
||||
"github.com/gravitational/teleport/lib/backend/memory"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
)
|
||||
@@ -193,6 +195,7 @@ func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
cmpopts.EquateEmpty(),
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(header.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(types.AppServerSpecV3{}, "ComponentFeatures"),
|
||||
// Ignore order.
|
||||
cmpopts.SortSlices(func(a, b types.ResourceWithLabels) bool { return a.GetName() < b.GetName() }),
|
||||
))
|
||||
@@ -225,6 +228,7 @@ func TestUnifiedResourceWatcher(t *testing.T) {
|
||||
cmpopts.EquateEmpty(),
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(header.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(types.AppServerSpecV3{}, "ComponentFeatures"),
|
||||
|
||||
// Ignore order.
|
||||
cmpopts.SortSlices(func(a, b types.ResourceWithLabels) bool { return a.GetName() < b.GetName() }),
|
||||
@@ -337,6 +341,7 @@ func TestUnifiedResourceCacheIterateResources(t *testing.T) {
|
||||
compareResourceOpts := []cmp.Option{cmpopts.EquateEmpty(),
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(header.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(types.AppServerSpecV3{}, "ComponentFeatures"),
|
||||
|
||||
// Ignore order.
|
||||
cmpopts.SortSlices(func(a, b types.ResourceWithLabels) bool { return a.GetName() < b.GetName() }),
|
||||
@@ -843,6 +848,124 @@ func TestUnifiedResourceWatcher_PreventDuplicates(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestUnifiedResourceCache_AppServerComponentFeaturesIntersection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
const appName = "aws-console-app"
|
||||
|
||||
makeAppServer := func(
|
||||
t *testing.T,
|
||||
hostID string,
|
||||
features *componentfeaturesv1.ComponentFeatures,
|
||||
) *types.AppServerV3 {
|
||||
t.Helper()
|
||||
srv, err := types.NewAppServerV3(
|
||||
types.Metadata{Name: appName},
|
||||
types.AppServerSpecV3{
|
||||
HostID: hostID,
|
||||
App: newApp(t, appName),
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
if features != nil {
|
||||
srv.SetComponentFeatures(features)
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
findAppServer := func(
|
||||
t *testing.T,
|
||||
w *services.UnifiedResourceCache,
|
||||
appName string,
|
||||
) types.AppServer {
|
||||
t.Helper()
|
||||
var (
|
||||
aggregatedServer types.AppServer
|
||||
count int
|
||||
)
|
||||
for srv, err := range w.AppServers(ctx, services.UnifiedResourcesIterateParams{}) {
|
||||
require.NoError(t, err)
|
||||
if srv.GetApp() == nil || srv.GetApp().GetName() != appName {
|
||||
continue
|
||||
}
|
||||
aggregatedServer = srv
|
||||
count++
|
||||
}
|
||||
require.Equal(t, 1, count, "expected exactly one AppServer for app %q", appName)
|
||||
require.NotNil(t, aggregatedServer, "expected non-nil AppServer for app %q", appName)
|
||||
return aggregatedServer
|
||||
}
|
||||
|
||||
createUnifiedResourceCache := func(t *testing.T, clt *client) *services.UnifiedResourceCache {
|
||||
t.Helper()
|
||||
w, err := services.NewUnifiedResourceCache(ctx, services.UnifiedResourceCacheConfig{
|
||||
ResourceWatcherConfig: services.ResourceWatcherConfig{
|
||||
Component: teleport.ComponentUnifiedResource,
|
||||
Client: clt,
|
||||
},
|
||||
ResourceGetter: clt,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
return w.IsInitialized()
|
||||
}, 5*time.Second, 10*time.Millisecond, "unified resource watcher never initialized")
|
||||
return w
|
||||
}
|
||||
|
||||
t.Run("feature advertised by only one AppServer is not advertised by the aggregated AppServer", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clt := newClient(t)
|
||||
|
||||
shared := componentfeatures.FeatureResourceConstraintsV1
|
||||
onlyOnOne := componentfeatures.FeatureID(9999)
|
||||
|
||||
appSrv1 := makeAppServer(t, "host-1", componentfeatures.New(shared, onlyOnOne))
|
||||
_, err := clt.UpsertApplicationServer(ctx, appSrv1)
|
||||
require.NoError(t, err)
|
||||
appSrv2 := makeAppServer(t, "host-2", componentfeatures.New(shared))
|
||||
_, err = clt.UpsertApplicationServer(ctx, appSrv2)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := createUnifiedResourceCache(t, clt)
|
||||
|
||||
// There should be a single AppServer for this app, with features = intersection(shared, onlyOnOne) = shared
|
||||
aggregatedServer := findAppServer(t, w, appName)
|
||||
cf := aggregatedServer.GetComponentFeatures()
|
||||
|
||||
require.NotNil(t, cf, "aggregated AppServer should have non-nil ComponentFeatures")
|
||||
require.ElementsMatch(t, []componentfeaturesv1.ComponentFeatureID{
|
||||
shared.ToProto(),
|
||||
}, cf.GetFeatures(), fmt.Sprintf("expected intersection to contain only '%s'", shared.String()))
|
||||
})
|
||||
|
||||
t.Run("any empty or nil feature set yields empty intersection", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clt := newClient(t)
|
||||
|
||||
shared := componentfeatures.FeatureResourceConstraintsV1
|
||||
|
||||
appSrv1 := makeAppServer(t, "host-1", componentfeatures.New(shared))
|
||||
_, err := clt.UpsertApplicationServer(ctx, appSrv1)
|
||||
require.NoError(t, err)
|
||||
appSrv2 := makeAppServer(t, "host-2", nil)
|
||||
_, err = clt.UpsertApplicationServer(ctx, appSrv2)
|
||||
require.NoError(t, err)
|
||||
|
||||
w := createUnifiedResourceCache(t, clt)
|
||||
|
||||
// There should be a single AppServer for this app, with features = intersection(shared, empty) = empty
|
||||
aggregatedServer := findAppServer(t, w, appName)
|
||||
cf := aggregatedServer.GetComponentFeatures()
|
||||
|
||||
require.NotNil(t, cf, "aggregated AppServer should have non-nil ComponentFeatures")
|
||||
require.Empty(t, cf.Features, "expected empty intersection when one of the feature sets is nil or empty")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnifiedResourceWatcher_DeleteEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1218,6 +1341,7 @@ func TestUnifiedResourceCacheIterateMCPServers(t *testing.T) {
|
||||
compareResourceOpts := []cmp.Option{cmpopts.EquateEmpty(),
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(header.Metadata{}, "Revision"),
|
||||
cmpopts.IgnoreFields(types.AppServerSpecV3{}, "ComponentFeatures"),
|
||||
|
||||
// Ignore order.
|
||||
cmpopts.SortSlices(func(a, b types.ResourceWithLabels) bool { return a.GetName() < b.GetName() }),
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/inventory"
|
||||
"github.com/gravitational/teleport/lib/labels"
|
||||
"github.com/gravitational/teleport/lib/reversetunnel"
|
||||
@@ -351,8 +352,13 @@ func (s *Server) getServerInfo(app types.Application) (*types.AppServerV3, error
|
||||
App: copy,
|
||||
ProxyIDs: s.c.ConnectedProxyGetter.GetProxyIDs(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return server, trace.Wrap(err)
|
||||
server.SetComponentFeatures(componentfeatures.ForAppServer(server))
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// getRotationState is a helper to return this server's CA rotation state.
|
||||
|
||||
@@ -503,7 +503,7 @@ func TestStart(t *testing.T) {
|
||||
|
||||
sort.Sort(types.AppServers(servers))
|
||||
require.Empty(t, cmp.Diff([]types.AppServer{serverAWS, serverAWSWithIntegration, serverFoo}, servers,
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Expires")))
|
||||
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Expires"), cmpopts.IgnoreFields(types.AppServerSpecV3{}, "ComponentFeatures")))
|
||||
|
||||
// Check the expiry time is correct.
|
||||
for _, server := range servers {
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
"github.com/gravitational/teleport/lib/bpf"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/inventory"
|
||||
@@ -353,6 +354,11 @@ func (s *Server) GetSELinuxEnabled() bool {
|
||||
return s.enableSELinux
|
||||
}
|
||||
|
||||
// GetProxyMode returns whether the server is started in SSH proxying mode.
|
||||
func (s *Server) GetProxyMode() bool {
|
||||
return s.proxyMode
|
||||
}
|
||||
|
||||
// ChildLogConfig returns the child log config.
|
||||
func (s *Server) ChildLogConfig() srv.ChildLogConfig {
|
||||
if s.childLogConfig != nil {
|
||||
@@ -1188,6 +1194,7 @@ func (s *Server) getBasicInfo() *types.ServerV2 {
|
||||
},
|
||||
}
|
||||
srv.SetPublicAddrs(utils.NetAddrsToStrings(s.publicAddrs))
|
||||
srv.SetComponentFeatures(componentfeatures.ForSSHServer(s))
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/client/webclient"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
|
||||
notificationsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/notifications/v1"
|
||||
summarizerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/summarizer/v1"
|
||||
@@ -87,6 +88,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
|
||||
"github.com/gravitational/teleport/lib/client/sso"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
dtconfig "github.com/gravitational/teleport/lib/devicetrust/config"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
@@ -3409,6 +3411,8 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
|
||||
getUserGroupLookup := h.getUserGroupLookup(request.Context(), clt)
|
||||
|
||||
clusterAuthProxyFeatures := h.getAuthProxyComponentFeaturesIntersection(request.Context())
|
||||
|
||||
unifiedResources := make([]any, 0, len(page))
|
||||
for _, enriched := range page {
|
||||
switch r := enriched.ResourceWithLabels.(type) {
|
||||
@@ -3441,6 +3445,11 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
proxyDNSName = utils.FindMatchingProxyDNS(request.Host, h.proxyDNSNames())
|
||||
}
|
||||
|
||||
// Compute end-to-end feature support for this app: only features that are supported by the AppServer *and*
|
||||
// by all required cluster hops (Auth + Proxy), so clients can hide features that would fail somewhere
|
||||
// along the request path.
|
||||
appComponentFeatures := componentfeatures.Intersect(r.GetComponentFeatures(), clusterAuthProxyFeatures)
|
||||
|
||||
app := ui.MakeApp(r.GetApp(), ui.MakeAppsConfig{
|
||||
LocalClusterName: h.auth.clusterName,
|
||||
LocalProxyDNSName: proxyDNSName,
|
||||
@@ -3449,6 +3458,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
UserGroupLookup: getUserGroupLookup(),
|
||||
Logger: h.logger,
|
||||
RequiresRequest: enriched.RequiresRequest,
|
||||
SupportedFeatures: appComponentFeatures,
|
||||
})
|
||||
unifiedResources = append(unifiedResources, app)
|
||||
case types.SAMLIdPServiceProvider:
|
||||
@@ -3486,6 +3496,51 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// getAuthProxyComponentFeaturesIntersection returns an intersection of supported ComponentFeatures
|
||||
// from all cluster Proxy and Auth servers.
|
||||
func (h *Handler) getAuthProxyComponentFeaturesIntersection(ctx context.Context) *componentfeaturesv1.ComponentFeatures {
|
||||
ap := h.GetAccessPoint()
|
||||
features := make([]*componentfeaturesv1.ComponentFeatures, 0)
|
||||
|
||||
allProxies, err := clientutils.CollectWithFallback(
|
||||
ctx,
|
||||
ap.ListProxyServers,
|
||||
func(context.Context) ([]types.Server, error) {
|
||||
//nolint:staticcheck // TODO(kiosion): DELETE IN 21.0.0
|
||||
return ap.GetProxies()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// If we fail to get proxies & can't be sure about feature support,
|
||||
// intersecting on `nil` ensures any intersection of ComponentFeatures will be empty.
|
||||
h.logger.ErrorContext(ctx, "Failed to get proxy servers to collect ComponentFeatures", "error", err)
|
||||
features = append(features, nil)
|
||||
} else {
|
||||
for _, srv := range allProxies {
|
||||
features = append(features, srv.GetComponentFeatures())
|
||||
}
|
||||
}
|
||||
|
||||
allAuthServers, err := clientutils.CollectWithFallback(
|
||||
ctx,
|
||||
ap.ListAuthServers,
|
||||
func(context.Context) ([]types.Server, error) {
|
||||
//nolint:staticcheck // TODO(kiosion): DELETE IN 21.0.0
|
||||
return ap.GetAuthServers()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.ErrorContext(ctx, "Failed to get auth servers to collect ComponentFeatures", "error", err)
|
||||
features = append(features, nil)
|
||||
} else {
|
||||
for _, srv := range allAuthServers {
|
||||
features = append(features, srv.GetComponentFeatures())
|
||||
}
|
||||
}
|
||||
|
||||
return componentfeatures.Intersect(features...)
|
||||
}
|
||||
|
||||
// clusterNodesGet returns a list of nodes for a given cluster site.
|
||||
func (h *Handler) clusterNodesGet(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, cluster reversetunnelclient.Cluster) (any, error) {
|
||||
// Get a client to the Auth Server with the logged in user's identity. The
|
||||
|
||||
@@ -93,6 +93,7 @@ import (
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
apievents "github.com/gravitational/teleport/api/types/events"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/api/utils/clientutils"
|
||||
"github.com/gravitational/teleport/api/utils/grpc/interceptors"
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
|
||||
@@ -114,6 +115,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/client/conntest"
|
||||
dbrepl "github.com/gravitational/teleport/lib/client/db/repl"
|
||||
"github.com/gravitational/teleport/lib/client/sso"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites/cryptosuitestest"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
@@ -1293,6 +1295,120 @@ func TestUserGroupsGet(t *testing.T) {
|
||||
}})
|
||||
}
|
||||
|
||||
func TestUnifiedResourcesGet_AppComponentFeatures(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
env := newWebPack(t, 1)
|
||||
proxy := env.proxies[0]
|
||||
|
||||
ctx := t.Context()
|
||||
username := "[email protected]"
|
||||
|
||||
u, err := user.Current()
|
||||
require.NoError(t, err)
|
||||
loginUser := u.Username
|
||||
|
||||
role := defaultRoleForNewUser(&types.UserV2{Metadata: types.Metadata{Name: username}}, loginUser)
|
||||
role.SetAppLabels(types.Allow, types.Labels{"env": []string{"prod"}})
|
||||
|
||||
pack := proxy.authPack(t, username, []types.Role{role})
|
||||
|
||||
// - cluster (auth/proxy) support `feature1`
|
||||
// - app has 2 appservers, both support `feature1` + `feature2`
|
||||
// - aggregated appserver features should = {feature1, feature2}
|
||||
// - final app supportedFeatureIDs should = {feature1}
|
||||
feature1 := componentfeatures.FeatureID(9998)
|
||||
feature2 := componentfeatures.FeatureID(9999)
|
||||
|
||||
{
|
||||
for srv := range clientutils.Resources(ctx, env.server.Auth().ListProxyServers) {
|
||||
srv.SetComponentFeatures(componentfeatures.New(feature1))
|
||||
err := env.server.Auth().UpsertProxy(ctx, srv)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
{
|
||||
for srv := range clientutils.Resources(ctx, env.server.Auth().ListAuthServers) {
|
||||
srv.SetComponentFeatures(componentfeatures.New(feature1))
|
||||
err := env.server.Auth().UpsertAuthServer(ctx, srv)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// AWS Console app + 2 app servers serving it
|
||||
appName := "aws-console-app"
|
||||
awsApp, err := types.NewAppV3(
|
||||
types.Metadata{
|
||||
Name: appName,
|
||||
Labels: map[string]string{
|
||||
"env": "prod",
|
||||
},
|
||||
},
|
||||
types.AppSpecV3{
|
||||
URI: "https://console.aws.amazon.com",
|
||||
Cloud: "AWS",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
awsSrv1, err := types.NewAppServerV3FromApp(awsApp, "localhost", "host-1")
|
||||
require.NoError(t, err)
|
||||
awsSrv1.SetComponentFeatures(componentfeatures.New(feature1, feature2))
|
||||
_, err = env.server.Auth().UpsertApplicationServer(ctx, awsSrv1)
|
||||
require.NoError(t, err)
|
||||
|
||||
awsSrv2, err := types.NewAppServerV3FromApp(awsApp, "localhost", "host-2")
|
||||
require.NoError(t, err)
|
||||
awsSrv2.SetComponentFeatures(componentfeatures.New(feature1, feature2))
|
||||
_, err = env.server.Auth().UpsertApplicationServer(ctx, awsSrv2)
|
||||
require.NoError(t, err)
|
||||
|
||||
clusterName := env.server.ClusterName()
|
||||
endpoint := pack.clt.Endpoint("webapi", "sites", clusterName, "resources")
|
||||
query := url.Values{"kinds": []string{types.KindApp}}
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return env.server.Auth().UnifiedResourceCache.IsInitialized()
|
||||
}, 5*time.Second, 50*time.Millisecond, "unified resource cache failed to initialize")
|
||||
|
||||
// Ensure aggregated AppServers' features are intersected as expected
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
var got []types.AppServer
|
||||
|
||||
for srv, err := range env.server.Auth().UnifiedResourceCache.AppServers(
|
||||
ctx, services.UnifiedResourcesIterateParams{},
|
||||
) {
|
||||
require.NoError(t, err)
|
||||
if srv.GetApp() == nil || srv.GetApp().GetName() != appName {
|
||||
continue
|
||||
}
|
||||
got = append(got, srv)
|
||||
}
|
||||
|
||||
require.Len(t, got, 1)
|
||||
|
||||
cf := got[0].GetComponentFeatures()
|
||||
require.NotNil(t, cf)
|
||||
require.ElementsMatch(t, componentfeatures.New(feature1, feature2).GetFeatures(), cf.GetFeatures())
|
||||
}, 5*time.Second, 50*time.Millisecond, "unified resource cache did not expose aggregated AppServer")
|
||||
|
||||
// Ensure returned UnifiedResource's features are intersected as expected (against all auth/proxy)
|
||||
re, err := pack.clt.Get(ctx, endpoint, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
var resp struct {
|
||||
Items []webui.App `json:"items"`
|
||||
}
|
||||
err = json.Unmarshal(re.Bytes(), &resp)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, resp.Items, 1)
|
||||
|
||||
app := resp.Items[0]
|
||||
require.True(t, app.AWSConsole)
|
||||
|
||||
require.ElementsMatch(t, []int{int(feature1)}, app.SupportedFeatureIDs)
|
||||
}
|
||||
|
||||
func TestUnifiedResourcesGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newWebPack(t, 1)
|
||||
|
||||
@@ -24,7 +24,9 @@ import (
|
||||
"log/slog"
|
||||
"sort"
|
||||
|
||||
componentfeaturesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/componentfeatures/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/ui"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
"github.com/gravitational/teleport/lib/utils/aws"
|
||||
@@ -81,6 +83,9 @@ type App struct {
|
||||
|
||||
// MCP includes MCP specific configuration.
|
||||
MCP *MCP `json:"mcp,omitempty"`
|
||||
|
||||
// SupportedFeatureIDs contains ComponentFeatures supported by this App and all other involved components.
|
||||
SupportedFeatureIDs []int `json:"supportedFeatureIds,omitempty"`
|
||||
}
|
||||
|
||||
// UserGroupAndDescription is a user group name and its description.
|
||||
@@ -135,6 +140,8 @@ type MakeAppsConfig struct {
|
||||
Logger *slog.Logger
|
||||
// RequireRequest indicates if a returned resource is only accessible after an access request
|
||||
RequiresRequest bool
|
||||
// SupportedFeatures contains ComponentFeatures supported by this App and all other involved components.
|
||||
SupportedFeatures *componentfeaturesv1.ComponentFeatures
|
||||
}
|
||||
|
||||
// MakeApp creates an application object for the WebUI.
|
||||
@@ -187,6 +194,7 @@ func MakeApp(app types.Application, c MakeAppsConfig) App {
|
||||
Integration: app.GetIntegration(),
|
||||
PermissionSets: permissionSets,
|
||||
UseAnyProxyPublicAddr: app.GetUseAnyProxyPublicAddr(),
|
||||
SupportedFeatureIDs: componentfeatures.ToIntegers(c.SupportedFeatures),
|
||||
}
|
||||
|
||||
if app.IsAWSConsole() {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/componentfeatures"
|
||||
"github.com/gravitational/teleport/lib/ui"
|
||||
)
|
||||
|
||||
@@ -41,6 +42,47 @@ func newApp(t *testing.T, name, publicAddr, description string, labels map[strin
|
||||
return app
|
||||
}
|
||||
|
||||
func TestMakeApp_SupportedFeatureIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, err := types.NewAppV3(
|
||||
types.Metadata{Name: "test-app"},
|
||||
types.AppSpecV3{URI: "localhost:8080"},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
baseCfg := MakeAppsConfig{
|
||||
LocalClusterName: "root",
|
||||
LocalProxyDNSName: "proxy.example.com",
|
||||
AppClusterName: "root",
|
||||
UserGroupLookup: map[string]types.UserGroup{},
|
||||
}
|
||||
|
||||
t.Run("nil SupportedFeatures yields empty SupportedFeatureIDs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := baseCfg
|
||||
cfg.SupportedFeatures = nil
|
||||
|
||||
out := MakeApp(app, cfg)
|
||||
require.Empty(t, out.SupportedFeatureIDs)
|
||||
})
|
||||
|
||||
t.Run("SupportedFeatures is converted to SupportedFeatureIDs", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f1 := componentfeatures.FeatureResourceConstraintsV1
|
||||
f2 := componentfeatures.FeatureID(9999)
|
||||
|
||||
cfg := baseCfg
|
||||
cfg.SupportedFeatures = componentfeatures.New(f1, f2)
|
||||
|
||||
out := MakeApp(app, cfg)
|
||||
|
||||
require.ElementsMatch(t, []int{int(f1), int(f2)}, out.SupportedFeatureIDs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMakeAppTypeFromSAMLApp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ResourceIconName } from 'design/ResourceIcon';
|
||||
import { TargetHealth } from 'gen-proto-ts/teleport/lib/teleterm/v1/target_health_pb';
|
||||
import { AppSubKind, NodeSubKind } from 'shared/services';
|
||||
import { DbProtocol } from 'shared/services/databases';
|
||||
import type { ComponentFeatureID } from 'shared/utils/componentFeatures';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports -- FIXME
|
||||
import { ResourceLabel } from 'teleport/services/agents';
|
||||
@@ -88,6 +89,7 @@ export type UnifiedResourceApp = {
|
||||
subKind?: AppSubKind;
|
||||
permissionSets?: PermissionSet[];
|
||||
mcp?: AppMCP;
|
||||
supportedFeatureIds?: ComponentFeatureID[];
|
||||
};
|
||||
|
||||
export interface UnifiedResourceDatabase {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2023 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ComponentFeatureID is an identifier for a specific feature supported
|
||||
* by a Teleport component.
|
||||
*/
|
||||
export enum ComponentFeatureID {
|
||||
/**
|
||||
* Unspecified is the unspecified feature ID.
|
||||
*/
|
||||
Unspecified = 0,
|
||||
/**
|
||||
* ResourceConstraintsV1 indicates support for Resource Constraints in
|
||||
* Access Requests, identity certificates, and AWS Console App resources.
|
||||
*/
|
||||
ResourceConstraintsV1 = 1,
|
||||
}
|
||||
Reference in New Issue
Block a user