mirror of
https://github.com/gravitational/teleport
synced 2024-10-18 08:13:24 +00:00
Update proxy features (#45979)
* Add feature watcher * Add test * Update godocs, fix typos, rename SupportEntitlementsCompatibility to BackfillFeatures * Godocs; rename start and stop functions * Use `Feature` prefix in var names instead of `License` * Fix lint * Fix TestGetWebConfig_LegacyFeatureLimits * Fix TestGetWebConfig_WithEntitlements * Fix tests and lint * Remove sync.Once * Add jitter * Remove Ping call from getUserContext * Move entitlements test to entitlements package * Apply suggestions from code review: godoc and tests improvement. Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com> * Improve tests * Shadow `t` in EventuallyWithT closure to avoid mistakes * Improve startFeatureWatcher godoc * Log features on update * Log features on update * Avoid race condition on test * Improve TODO comment Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com> * Use handler config context * Start feature watcher in NewHandler * Improve startFeatureWatcher godocs * Add TODO to unexport SetClusterFeatures * Remove featureWatcherReady * Use require in require.EventuallyWithT in cases where an error is not expected * Use return of assert.NoError` to return early on require.EventuallyWithT --------- Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com> Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com>
This commit is contained in:
parent
5f4eaf3130
commit
75bc1f2cc7
|
@ -16,6 +16,8 @@
|
|||
|
||||
package entitlements
|
||||
|
||||
import "github.com/gravitational/teleport/api/client/proto"
|
||||
|
||||
type EntitlementKind string
|
||||
|
||||
// The EntitlementKind list should be 1:1 with the Features & FeatureStrings in salescenter/product/product.go,
|
||||
|
@ -57,3 +59,67 @@ var AllEntitlements = []EntitlementKind{
|
|||
ExternalAuditStorage, FeatureHiding, HSM, Identity, JoinActiveSessions, K8s, MobileDeviceManagement, OIDC, OktaSCIM,
|
||||
OktaUserSync, Policy, SAML, SessionLocks, UpsellAlert, UsageReporting, LicenseAutoUpdate,
|
||||
}
|
||||
|
||||
// BackfillFeatures ensures entitlements are backwards compatible.
|
||||
// If Entitlements are present, there are no changes.
|
||||
// If Entitlements are not present, it sets the entitlements based on legacy field values.
|
||||
// TODO(michellescripts) DELETE IN 18.0.0
|
||||
func BackfillFeatures(features *proto.Features) {
|
||||
if len(features.Entitlements) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
features.Entitlements = getBaseEntitlements(features.GetEntitlements())
|
||||
|
||||
// Entitlements: All records are {enabled: false}; update to equal legacy feature value
|
||||
features.Entitlements[string(ExternalAuditStorage)] = &proto.EntitlementInfo{Enabled: features.GetExternalAuditStorage()}
|
||||
features.Entitlements[string(FeatureHiding)] = &proto.EntitlementInfo{Enabled: features.GetFeatureHiding()}
|
||||
features.Entitlements[string(Identity)] = &proto.EntitlementInfo{Enabled: features.GetIdentityGovernance()}
|
||||
features.Entitlements[string(JoinActiveSessions)] = &proto.EntitlementInfo{Enabled: features.GetJoinActiveSessions()}
|
||||
features.Entitlements[string(MobileDeviceManagement)] = &proto.EntitlementInfo{Enabled: features.GetMobileDeviceManagement()}
|
||||
features.Entitlements[string(OIDC)] = &proto.EntitlementInfo{Enabled: features.GetOIDC()}
|
||||
features.Entitlements[string(Policy)] = &proto.EntitlementInfo{Enabled: features.GetPolicy().GetEnabled()}
|
||||
features.Entitlements[string(SAML)] = &proto.EntitlementInfo{Enabled: features.GetSAML()}
|
||||
features.Entitlements[string(K8s)] = &proto.EntitlementInfo{Enabled: features.GetKubernetes()}
|
||||
features.Entitlements[string(App)] = &proto.EntitlementInfo{Enabled: features.GetApp()}
|
||||
features.Entitlements[string(DB)] = &proto.EntitlementInfo{Enabled: features.GetDB()}
|
||||
features.Entitlements[string(Desktop)] = &proto.EntitlementInfo{Enabled: features.GetDesktop()}
|
||||
features.Entitlements[string(HSM)] = &proto.EntitlementInfo{Enabled: features.GetHSM()}
|
||||
|
||||
// set default Identity fields to legacy feature value
|
||||
features.Entitlements[string(AccessLists)] = &proto.EntitlementInfo{Enabled: true, Limit: features.GetAccessList().GetCreateLimit()}
|
||||
features.Entitlements[string(AccessMonitoring)] = &proto.EntitlementInfo{Enabled: features.GetAccessMonitoring().GetEnabled(), Limit: features.GetAccessMonitoring().GetMaxReportRangeLimit()}
|
||||
features.Entitlements[string(AccessRequests)] = &proto.EntitlementInfo{Enabled: features.GetAccessRequests().MonthlyRequestLimit > 0, Limit: features.GetAccessRequests().GetMonthlyRequestLimit()}
|
||||
features.Entitlements[string(DeviceTrust)] = &proto.EntitlementInfo{Enabled: features.GetDeviceTrust().GetEnabled(), Limit: features.GetDeviceTrust().GetDevicesUsageLimit()}
|
||||
// override Identity Package features if Identity is enabled: set true and clear limit
|
||||
if features.GetIdentityGovernance() {
|
||||
features.Entitlements[string(AccessLists)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(AccessMonitoring)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(AccessRequests)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(DeviceTrust)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(OktaSCIM)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(OktaUserSync)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(SessionLocks)] = &proto.EntitlementInfo{Enabled: true}
|
||||
}
|
||||
}
|
||||
|
||||
// getBaseEntitlements takes a cloud entitlement set and returns a modules Entitlement set
|
||||
func getBaseEntitlements(protoEntitlements map[string]*proto.EntitlementInfo) map[string]*proto.EntitlementInfo {
|
||||
all := AllEntitlements
|
||||
result := make(map[string]*proto.EntitlementInfo, len(all))
|
||||
|
||||
for _, e := range all {
|
||||
al, ok := protoEntitlements[string(e)]
|
||||
if !ok {
|
||||
result[string(e)] = &proto.EntitlementInfo{}
|
||||
continue
|
||||
}
|
||||
|
||||
result[string(e)] = &proto.EntitlementInfo{
|
||||
Enabled: al.Enabled,
|
||||
Limit: al.Limit,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
286
entitlements/entitlements_test.go
Normal file
286
entitlements/entitlements_test.go
Normal file
|
@ -0,0 +1,286 @@
|
|||
// Teleport
|
||||
// Copyright (C) 2024 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 entitlements
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
)
|
||||
|
||||
func TestBackfillFeatures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
features *proto.Features
|
||||
expected map[string]*proto.EntitlementInfo
|
||||
}{
|
||||
{
|
||||
name: "entitlements present; keeps entitlement values",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: nil,
|
||||
AccessRequests: nil,
|
||||
AccessList: nil,
|
||||
AccessMonitoring: nil,
|
||||
Policy: nil,
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: false,
|
||||
App: false,
|
||||
DB: false,
|
||||
OIDC: false,
|
||||
SAML: false,
|
||||
AccessControls: false,
|
||||
AdvancedAccessWorkflows: false,
|
||||
Cloud: false,
|
||||
HSM: false,
|
||||
Desktop: false,
|
||||
RecoveryCodes: false,
|
||||
Plugins: false,
|
||||
AutomaticUpgrades: false,
|
||||
IsUsageBased: false,
|
||||
Assist: false,
|
||||
FeatureHiding: false,
|
||||
IdentityGovernance: false,
|
||||
AccessGraph: false,
|
||||
Questionnaire: false,
|
||||
IsStripeManaged: false,
|
||||
ExternalAuditStorage: false,
|
||||
JoinActiveSessions: false,
|
||||
MobileDeviceManagement: false,
|
||||
AccessMonitoringConfigured: false,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{
|
||||
string(AccessLists): {Enabled: true, Limit: 111},
|
||||
string(AccessMonitoring): {Enabled: true, Limit: 2113},
|
||||
string(AccessRequests): {Enabled: true, Limit: 39},
|
||||
string(App): {Enabled: false},
|
||||
string(CloudAuditLogRetention): {Enabled: true},
|
||||
string(DB): {Enabled: true},
|
||||
string(Desktop): {Enabled: true},
|
||||
string(DeviceTrust): {Enabled: true, Limit: 103},
|
||||
string(ExternalAuditStorage): {Enabled: true},
|
||||
string(FeatureHiding): {Enabled: true},
|
||||
string(HSM): {Enabled: true},
|
||||
string(Identity): {Enabled: true},
|
||||
string(JoinActiveSessions): {Enabled: true},
|
||||
string(K8s): {Enabled: true},
|
||||
string(MobileDeviceManagement): {Enabled: true},
|
||||
string(OIDC): {Enabled: true},
|
||||
string(OktaSCIM): {Enabled: true},
|
||||
string(OktaUserSync): {Enabled: true},
|
||||
string(Policy): {Enabled: true},
|
||||
string(SAML): {Enabled: true},
|
||||
string(SessionLocks): {Enabled: true},
|
||||
string(UpsellAlert): {Enabled: true},
|
||||
string(UsageReporting): {Enabled: true},
|
||||
string(LicenseAutoUpdate): {Enabled: true},
|
||||
},
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(AccessLists): {Enabled: true, Limit: 111},
|
||||
string(AccessMonitoring): {Enabled: true, Limit: 2113},
|
||||
string(AccessRequests): {Enabled: true, Limit: 39},
|
||||
string(App): {Enabled: false},
|
||||
string(CloudAuditLogRetention): {Enabled: true},
|
||||
string(DB): {Enabled: true},
|
||||
string(Desktop): {Enabled: true},
|
||||
string(DeviceTrust): {Enabled: true, Limit: 103},
|
||||
string(ExternalAuditStorage): {Enabled: true},
|
||||
string(FeatureHiding): {Enabled: true},
|
||||
string(HSM): {Enabled: true},
|
||||
string(Identity): {Enabled: true},
|
||||
string(JoinActiveSessions): {Enabled: true},
|
||||
string(K8s): {Enabled: true},
|
||||
string(MobileDeviceManagement): {Enabled: true},
|
||||
string(OIDC): {Enabled: true},
|
||||
string(OktaSCIM): {Enabled: true},
|
||||
string(OktaUserSync): {Enabled: true},
|
||||
string(Policy): {Enabled: true},
|
||||
string(SAML): {Enabled: true},
|
||||
string(SessionLocks): {Enabled: true},
|
||||
string(UpsellAlert): {Enabled: true},
|
||||
string(UsageReporting): {Enabled: true},
|
||||
string(LicenseAutoUpdate): {Enabled: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entitlements not present; identity on - sets legacy fields & drops limits",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: &proto.DeviceTrustFeature{
|
||||
Enabled: true,
|
||||
DevicesUsageLimit: 33,
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{
|
||||
MonthlyRequestLimit: 22,
|
||||
},
|
||||
AccessList: &proto.AccessListFeature{
|
||||
CreateLimit: 44,
|
||||
},
|
||||
AccessMonitoring: &proto.AccessMonitoringFeature{
|
||||
Enabled: true,
|
||||
MaxReportRangeLimit: 55,
|
||||
},
|
||||
Policy: &proto.PolicyFeature{
|
||||
Enabled: true,
|
||||
},
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: true,
|
||||
App: true,
|
||||
DB: true,
|
||||
OIDC: true,
|
||||
SAML: true,
|
||||
AccessControls: true,
|
||||
AdvancedAccessWorkflows: true,
|
||||
Cloud: true,
|
||||
HSM: true,
|
||||
Desktop: true,
|
||||
RecoveryCodes: true,
|
||||
Plugins: true,
|
||||
AutomaticUpgrades: true,
|
||||
IsUsageBased: true,
|
||||
Assist: true,
|
||||
FeatureHiding: true,
|
||||
IdentityGovernance: true,
|
||||
AccessGraph: true,
|
||||
Questionnaire: true,
|
||||
IsStripeManaged: true,
|
||||
ExternalAuditStorage: true,
|
||||
JoinActiveSessions: true,
|
||||
MobileDeviceManagement: true,
|
||||
AccessMonitoringConfigured: true,
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(AccessLists): {Enabled: true},
|
||||
string(AccessMonitoring): {Enabled: true},
|
||||
string(AccessRequests): {Enabled: true},
|
||||
string(App): {Enabled: true},
|
||||
string(DB): {Enabled: true},
|
||||
string(Desktop): {Enabled: true},
|
||||
string(DeviceTrust): {Enabled: true},
|
||||
string(ExternalAuditStorage): {Enabled: true},
|
||||
string(FeatureHiding): {Enabled: true},
|
||||
string(HSM): {Enabled: true},
|
||||
string(Identity): {Enabled: true},
|
||||
string(JoinActiveSessions): {Enabled: true},
|
||||
string(K8s): {Enabled: true},
|
||||
string(MobileDeviceManagement): {Enabled: true},
|
||||
string(OIDC): {Enabled: true},
|
||||
string(OktaSCIM): {Enabled: true},
|
||||
string(OktaUserSync): {Enabled: true},
|
||||
string(Policy): {Enabled: true},
|
||||
string(SAML): {Enabled: true},
|
||||
string(SessionLocks): {Enabled: true},
|
||||
// defaults, no legacy equivalent
|
||||
string(UsageReporting): {Enabled: false},
|
||||
string(UpsellAlert): {Enabled: false},
|
||||
string(CloudAuditLogRetention): {Enabled: false},
|
||||
string(LicenseAutoUpdate): {Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entitlements not present; identity off - sets legacy fields",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: &proto.DeviceTrustFeature{
|
||||
Enabled: true,
|
||||
DevicesUsageLimit: 33,
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{
|
||||
MonthlyRequestLimit: 22,
|
||||
},
|
||||
AccessList: &proto.AccessListFeature{
|
||||
CreateLimit: 44,
|
||||
},
|
||||
AccessMonitoring: &proto.AccessMonitoringFeature{
|
||||
Enabled: true,
|
||||
MaxReportRangeLimit: 55,
|
||||
},
|
||||
Policy: &proto.PolicyFeature{
|
||||
Enabled: true,
|
||||
},
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: true,
|
||||
App: true,
|
||||
DB: true,
|
||||
OIDC: true,
|
||||
SAML: true,
|
||||
AccessControls: true,
|
||||
AdvancedAccessWorkflows: true,
|
||||
Cloud: true,
|
||||
HSM: true,
|
||||
Desktop: true,
|
||||
RecoveryCodes: true,
|
||||
Plugins: true,
|
||||
AutomaticUpgrades: true,
|
||||
IsUsageBased: true,
|
||||
Assist: true,
|
||||
FeatureHiding: true,
|
||||
IdentityGovernance: false,
|
||||
AccessGraph: true,
|
||||
Questionnaire: true,
|
||||
IsStripeManaged: true,
|
||||
ExternalAuditStorage: true,
|
||||
JoinActiveSessions: true,
|
||||
MobileDeviceManagement: true,
|
||||
AccessMonitoringConfigured: true,
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(AccessLists): {Enabled: true, Limit: 44},
|
||||
string(AccessMonitoring): {Enabled: true, Limit: 55},
|
||||
string(AccessRequests): {Enabled: true, Limit: 22},
|
||||
string(DeviceTrust): {Enabled: true, Limit: 33},
|
||||
string(App): {Enabled: true},
|
||||
string(DB): {Enabled: true},
|
||||
string(Desktop): {Enabled: true},
|
||||
string(ExternalAuditStorage): {Enabled: true},
|
||||
string(FeatureHiding): {Enabled: true},
|
||||
string(HSM): {Enabled: true},
|
||||
string(JoinActiveSessions): {Enabled: true},
|
||||
string(K8s): {Enabled: true},
|
||||
string(MobileDeviceManagement): {Enabled: true},
|
||||
string(OIDC): {Enabled: true},
|
||||
string(Policy): {Enabled: true},
|
||||
string(SAML): {Enabled: true},
|
||||
|
||||
// defaults, no legacy equivalent
|
||||
string(UsageReporting): {Enabled: false},
|
||||
string(UpsellAlert): {Enabled: false},
|
||||
string(CloudAuditLogRetention): {Enabled: false},
|
||||
string(LicenseAutoUpdate): {Enabled: false},
|
||||
// Identity off, fields false
|
||||
string(Identity): {Enabled: false},
|
||||
string(SessionLocks): {Enabled: false},
|
||||
string(OktaSCIM): {Enabled: false},
|
||||
string(OktaUserSync): {Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cloned := apiutils.CloneProtoMsg(tt.features)
|
||||
|
||||
BackfillFeatures(cloned)
|
||||
require.Equal(t, tt.expected, cloned.Entitlements)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1087,7 +1087,7 @@ func (process *TeleportProcess) getConnector(clientIdentity, serverIdentity *sta
|
|||
// Set cluster features and return successfully with a working connector.
|
||||
// TODO(michellescripts) remove clone & compatibility check in v18
|
||||
cloned := apiutils.CloneProtoMsg(pingResponse.GetServerFeatures())
|
||||
supportEntitlementsCompatibility(cloned)
|
||||
entitlements.BackfillFeatures(cloned)
|
||||
process.setClusterFeatures(cloned)
|
||||
process.setAuthSubjectiveAddr(pingResponse.RemoteAddr)
|
||||
process.logger.InfoContext(process.ExitContext(), "features loaded from auth server", "identity", clientIdentity.ID.Role, "features", pingResponse.GetServerFeatures())
|
||||
|
@ -1096,70 +1096,6 @@ func (process *TeleportProcess) getConnector(clientIdentity, serverIdentity *sta
|
|||
return newConn, nil
|
||||
}
|
||||
|
||||
// supportEntitlementsCompatibility ensures entitlements are backwards compatible
|
||||
// If Entitlements are present, there are no changes
|
||||
// If Entitlements are not present, sets the entitlements fields to legacy field values
|
||||
// TODO(michellescripts) remove in v18
|
||||
func supportEntitlementsCompatibility(features *proto.Features) {
|
||||
if len(features.Entitlements) > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
features.Entitlements = getBaseEntitlements(features.GetEntitlements())
|
||||
|
||||
// Entitlements: All records are {enabled: false}; update to equal legacy feature value
|
||||
features.Entitlements[string(entitlements.ExternalAuditStorage)] = &proto.EntitlementInfo{Enabled: features.GetExternalAuditStorage()}
|
||||
features.Entitlements[string(entitlements.FeatureHiding)] = &proto.EntitlementInfo{Enabled: features.GetFeatureHiding()}
|
||||
features.Entitlements[string(entitlements.Identity)] = &proto.EntitlementInfo{Enabled: features.GetIdentityGovernance()}
|
||||
features.Entitlements[string(entitlements.JoinActiveSessions)] = &proto.EntitlementInfo{Enabled: features.GetJoinActiveSessions()}
|
||||
features.Entitlements[string(entitlements.MobileDeviceManagement)] = &proto.EntitlementInfo{Enabled: features.GetMobileDeviceManagement()}
|
||||
features.Entitlements[string(entitlements.OIDC)] = &proto.EntitlementInfo{Enabled: features.GetOIDC()}
|
||||
features.Entitlements[string(entitlements.Policy)] = &proto.EntitlementInfo{Enabled: features.GetPolicy().GetEnabled()}
|
||||
features.Entitlements[string(entitlements.SAML)] = &proto.EntitlementInfo{Enabled: features.GetSAML()}
|
||||
features.Entitlements[string(entitlements.K8s)] = &proto.EntitlementInfo{Enabled: features.GetKubernetes()}
|
||||
features.Entitlements[string(entitlements.App)] = &proto.EntitlementInfo{Enabled: features.GetApp()}
|
||||
features.Entitlements[string(entitlements.DB)] = &proto.EntitlementInfo{Enabled: features.GetDB()}
|
||||
features.Entitlements[string(entitlements.Desktop)] = &proto.EntitlementInfo{Enabled: features.GetDesktop()}
|
||||
features.Entitlements[string(entitlements.HSM)] = &proto.EntitlementInfo{Enabled: features.GetHSM()}
|
||||
|
||||
// set default Identity fields to legacy feature value
|
||||
features.Entitlements[string(entitlements.AccessLists)] = &proto.EntitlementInfo{Enabled: true, Limit: features.GetAccessList().GetCreateLimit()}
|
||||
features.Entitlements[string(entitlements.AccessMonitoring)] = &proto.EntitlementInfo{Enabled: features.GetAccessMonitoring().GetEnabled(), Limit: features.GetAccessMonitoring().GetMaxReportRangeLimit()}
|
||||
features.Entitlements[string(entitlements.AccessRequests)] = &proto.EntitlementInfo{Enabled: features.GetAccessRequests().MonthlyRequestLimit > 0, Limit: features.GetAccessRequests().GetMonthlyRequestLimit()}
|
||||
features.Entitlements[string(entitlements.DeviceTrust)] = &proto.EntitlementInfo{Enabled: features.GetDeviceTrust().GetEnabled(), Limit: features.GetDeviceTrust().GetDevicesUsageLimit()}
|
||||
// override Identity Package features if Identity is enabled: set true and clear limit
|
||||
if features.GetIdentityGovernance() {
|
||||
features.Entitlements[string(entitlements.AccessLists)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.AccessMonitoring)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.AccessRequests)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.DeviceTrust)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.OktaSCIM)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.OktaUserSync)] = &proto.EntitlementInfo{Enabled: true}
|
||||
features.Entitlements[string(entitlements.SessionLocks)] = &proto.EntitlementInfo{Enabled: true}
|
||||
}
|
||||
}
|
||||
|
||||
// getBaseEntitlements takes a cloud entitlement set and returns a modules Entitlement set
|
||||
func getBaseEntitlements(protoEntitlements map[string]*proto.EntitlementInfo) map[string]*proto.EntitlementInfo {
|
||||
all := entitlements.AllEntitlements
|
||||
result := make(map[string]*proto.EntitlementInfo, len(all))
|
||||
|
||||
for _, e := range all {
|
||||
al, ok := protoEntitlements[string(e)]
|
||||
if !ok {
|
||||
result[string(e)] = &proto.EntitlementInfo{}
|
||||
continue
|
||||
}
|
||||
|
||||
result[string(e)] = &proto.EntitlementInfo{
|
||||
Enabled: al.Enabled,
|
||||
Limit: al.Limit,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// newClient attempts to connect to either the proxy server or auth server
|
||||
// For config v3 and onwards, it will only connect to either the proxy (via tunnel) or the auth server (direct),
|
||||
// depending on what was specified in the config.
|
||||
|
|
|
@ -1,288 +0,0 @@
|
|||
// Teleport
|
||||
// Copyright (C) 2024 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 service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/entitlements"
|
||||
)
|
||||
|
||||
func Test_supportEntitlementsCompatibility(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
features *proto.Features
|
||||
expected map[string]*proto.EntitlementInfo
|
||||
}{
|
||||
{
|
||||
name: "entitlements present; keeps entitlement values",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: nil,
|
||||
AccessRequests: nil,
|
||||
AccessList: nil,
|
||||
AccessMonitoring: nil,
|
||||
Policy: nil,
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: false,
|
||||
App: false,
|
||||
DB: false,
|
||||
OIDC: false,
|
||||
SAML: false,
|
||||
AccessControls: false,
|
||||
AdvancedAccessWorkflows: false,
|
||||
Cloud: false,
|
||||
HSM: false,
|
||||
Desktop: false,
|
||||
RecoveryCodes: false,
|
||||
Plugins: false,
|
||||
AutomaticUpgrades: false,
|
||||
IsUsageBased: false,
|
||||
Assist: false,
|
||||
FeatureHiding: false,
|
||||
IdentityGovernance: false,
|
||||
AccessGraph: false,
|
||||
Questionnaire: false,
|
||||
IsStripeManaged: false,
|
||||
ExternalAuditStorage: false,
|
||||
JoinActiveSessions: false,
|
||||
MobileDeviceManagement: false,
|
||||
AccessMonitoringConfigured: false,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.AccessLists): {Enabled: true, Limit: 111},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true, Limit: 2113},
|
||||
string(entitlements.AccessRequests): {Enabled: true, Limit: 39},
|
||||
string(entitlements.App): {Enabled: false},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: true},
|
||||
string(entitlements.DB): {Enabled: true},
|
||||
string(entitlements.Desktop): {Enabled: true},
|
||||
string(entitlements.DeviceTrust): {Enabled: true, Limit: 103},
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.FeatureHiding): {Enabled: true},
|
||||
string(entitlements.HSM): {Enabled: true},
|
||||
string(entitlements.Identity): {Enabled: true},
|
||||
string(entitlements.JoinActiveSessions): {Enabled: true},
|
||||
string(entitlements.K8s): {Enabled: true},
|
||||
string(entitlements.MobileDeviceManagement): {Enabled: true},
|
||||
string(entitlements.OIDC): {Enabled: true},
|
||||
string(entitlements.OktaSCIM): {Enabled: true},
|
||||
string(entitlements.OktaUserSync): {Enabled: true},
|
||||
string(entitlements.Policy): {Enabled: true},
|
||||
string(entitlements.SAML): {Enabled: true},
|
||||
string(entitlements.SessionLocks): {Enabled: true},
|
||||
string(entitlements.UpsellAlert): {Enabled: true},
|
||||
string(entitlements.UsageReporting): {Enabled: true},
|
||||
string(entitlements.LicenseAutoUpdate): {Enabled: true},
|
||||
},
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.AccessLists): {Enabled: true, Limit: 111},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true, Limit: 2113},
|
||||
string(entitlements.AccessRequests): {Enabled: true, Limit: 39},
|
||||
string(entitlements.App): {Enabled: false},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: true},
|
||||
string(entitlements.DB): {Enabled: true},
|
||||
string(entitlements.Desktop): {Enabled: true},
|
||||
string(entitlements.DeviceTrust): {Enabled: true, Limit: 103},
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.FeatureHiding): {Enabled: true},
|
||||
string(entitlements.HSM): {Enabled: true},
|
||||
string(entitlements.Identity): {Enabled: true},
|
||||
string(entitlements.JoinActiveSessions): {Enabled: true},
|
||||
string(entitlements.K8s): {Enabled: true},
|
||||
string(entitlements.MobileDeviceManagement): {Enabled: true},
|
||||
string(entitlements.OIDC): {Enabled: true},
|
||||
string(entitlements.OktaSCIM): {Enabled: true},
|
||||
string(entitlements.OktaUserSync): {Enabled: true},
|
||||
string(entitlements.Policy): {Enabled: true},
|
||||
string(entitlements.SAML): {Enabled: true},
|
||||
string(entitlements.SessionLocks): {Enabled: true},
|
||||
string(entitlements.UpsellAlert): {Enabled: true},
|
||||
string(entitlements.UsageReporting): {Enabled: true},
|
||||
string(entitlements.LicenseAutoUpdate): {Enabled: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entitlements not present; identity on - sets legacy fields & drops limits",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: &proto.DeviceTrustFeature{
|
||||
Enabled: true,
|
||||
DevicesUsageLimit: 33,
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{
|
||||
MonthlyRequestLimit: 22,
|
||||
},
|
||||
AccessList: &proto.AccessListFeature{
|
||||
CreateLimit: 44,
|
||||
},
|
||||
AccessMonitoring: &proto.AccessMonitoringFeature{
|
||||
Enabled: true,
|
||||
MaxReportRangeLimit: 55,
|
||||
},
|
||||
Policy: &proto.PolicyFeature{
|
||||
Enabled: true,
|
||||
},
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: true,
|
||||
App: true,
|
||||
DB: true,
|
||||
OIDC: true,
|
||||
SAML: true,
|
||||
AccessControls: true,
|
||||
AdvancedAccessWorkflows: true,
|
||||
Cloud: true,
|
||||
HSM: true,
|
||||
Desktop: true,
|
||||
RecoveryCodes: true,
|
||||
Plugins: true,
|
||||
AutomaticUpgrades: true,
|
||||
IsUsageBased: true,
|
||||
Assist: true,
|
||||
FeatureHiding: true,
|
||||
IdentityGovernance: true,
|
||||
AccessGraph: true,
|
||||
Questionnaire: true,
|
||||
IsStripeManaged: true,
|
||||
ExternalAuditStorage: true,
|
||||
JoinActiveSessions: true,
|
||||
MobileDeviceManagement: true,
|
||||
AccessMonitoringConfigured: true,
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.AccessLists): {Enabled: true},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true},
|
||||
string(entitlements.AccessRequests): {Enabled: true},
|
||||
string(entitlements.App): {Enabled: true},
|
||||
string(entitlements.DB): {Enabled: true},
|
||||
string(entitlements.Desktop): {Enabled: true},
|
||||
string(entitlements.DeviceTrust): {Enabled: true},
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.FeatureHiding): {Enabled: true},
|
||||
string(entitlements.HSM): {Enabled: true},
|
||||
string(entitlements.Identity): {Enabled: true},
|
||||
string(entitlements.JoinActiveSessions): {Enabled: true},
|
||||
string(entitlements.K8s): {Enabled: true},
|
||||
string(entitlements.MobileDeviceManagement): {Enabled: true},
|
||||
string(entitlements.OIDC): {Enabled: true},
|
||||
string(entitlements.OktaSCIM): {Enabled: true},
|
||||
string(entitlements.OktaUserSync): {Enabled: true},
|
||||
string(entitlements.Policy): {Enabled: true},
|
||||
string(entitlements.SAML): {Enabled: true},
|
||||
string(entitlements.SessionLocks): {Enabled: true},
|
||||
// defaults, no legacy equivalent
|
||||
string(entitlements.UsageReporting): {Enabled: false},
|
||||
string(entitlements.UpsellAlert): {Enabled: false},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: false},
|
||||
string(entitlements.LicenseAutoUpdate): {Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entitlements not present; identity off - sets legacy fields",
|
||||
features: &proto.Features{
|
||||
DeviceTrust: &proto.DeviceTrustFeature{
|
||||
Enabled: true,
|
||||
DevicesUsageLimit: 33,
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{
|
||||
MonthlyRequestLimit: 22,
|
||||
},
|
||||
AccessList: &proto.AccessListFeature{
|
||||
CreateLimit: 44,
|
||||
},
|
||||
AccessMonitoring: &proto.AccessMonitoringFeature{
|
||||
Enabled: true,
|
||||
MaxReportRangeLimit: 55,
|
||||
},
|
||||
Policy: &proto.PolicyFeature{
|
||||
Enabled: true,
|
||||
},
|
||||
CustomTheme: "",
|
||||
ProductType: 0,
|
||||
SupportType: 0,
|
||||
Kubernetes: true,
|
||||
App: true,
|
||||
DB: true,
|
||||
OIDC: true,
|
||||
SAML: true,
|
||||
AccessControls: true,
|
||||
AdvancedAccessWorkflows: true,
|
||||
Cloud: true,
|
||||
HSM: true,
|
||||
Desktop: true,
|
||||
RecoveryCodes: true,
|
||||
Plugins: true,
|
||||
AutomaticUpgrades: true,
|
||||
IsUsageBased: true,
|
||||
Assist: true,
|
||||
FeatureHiding: true,
|
||||
IdentityGovernance: false,
|
||||
AccessGraph: true,
|
||||
Questionnaire: true,
|
||||
IsStripeManaged: true,
|
||||
ExternalAuditStorage: true,
|
||||
JoinActiveSessions: true,
|
||||
MobileDeviceManagement: true,
|
||||
AccessMonitoringConfigured: true,
|
||||
},
|
||||
expected: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.AccessLists): {Enabled: true, Limit: 44},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true, Limit: 55},
|
||||
string(entitlements.AccessRequests): {Enabled: true, Limit: 22},
|
||||
string(entitlements.DeviceTrust): {Enabled: true, Limit: 33},
|
||||
string(entitlements.App): {Enabled: true},
|
||||
string(entitlements.DB): {Enabled: true},
|
||||
string(entitlements.Desktop): {Enabled: true},
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.FeatureHiding): {Enabled: true},
|
||||
string(entitlements.HSM): {Enabled: true},
|
||||
string(entitlements.JoinActiveSessions): {Enabled: true},
|
||||
string(entitlements.K8s): {Enabled: true},
|
||||
string(entitlements.MobileDeviceManagement): {Enabled: true},
|
||||
string(entitlements.OIDC): {Enabled: true},
|
||||
string(entitlements.Policy): {Enabled: true},
|
||||
string(entitlements.SAML): {Enabled: true},
|
||||
|
||||
// defaults, no legacy equivalent
|
||||
string(entitlements.UsageReporting): {Enabled: false},
|
||||
string(entitlements.UpsellAlert): {Enabled: false},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: false},
|
||||
string(entitlements.LicenseAutoUpdate): {Enabled: false},
|
||||
|
||||
// Identity off, fields false
|
||||
string(entitlements.Identity): {Enabled: false},
|
||||
string(entitlements.SessionLocks): {Enabled: false},
|
||||
string(entitlements.OktaSCIM): {Enabled: false},
|
||||
string(entitlements.OktaUserSync): {Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cloned := apiutils.CloneProtoMsg(tt.features)
|
||||
|
||||
supportEntitlementsCompatibility(cloned)
|
||||
require.Equal(t, tt.expected, cloned.Entitlements)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -4595,6 +4595,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
|||
TracerProvider: process.TracingProvider,
|
||||
AutomaticUpgradesChannels: cfg.Proxy.AutomaticUpgradesChannels,
|
||||
IntegrationAppHandler: connectionsHandler,
|
||||
FeatureWatchInterval: utils.HalfJitter(web.DefaultFeatureWatchInterval * 2),
|
||||
}
|
||||
webHandler, err := web.NewHandler(webConfig)
|
||||
if err != nil {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
|
@ -124,6 +125,9 @@ const (
|
|||
IncludedResourceModeRequestable = "requestable"
|
||||
// IncludedResourceModeAll describes that all resources, requestable and available, should be returned.
|
||||
IncludedResourceModeAll = "all"
|
||||
// DefaultFeatureWatchInterval is the default time in which the feature watcher
|
||||
// should ping the auth server to check for updated features
|
||||
DefaultFeatureWatchInterval = time.Minute * 5
|
||||
)
|
||||
|
||||
// healthCheckAppServerFunc defines a function used to perform a health check
|
||||
|
@ -153,12 +157,8 @@ type Handler struct {
|
|||
// userConns tracks amount of current active connections with user certificates.
|
||||
userConns atomic.Int32
|
||||
|
||||
// ClusterFeatures contain flags for supported and unsupported features.
|
||||
// Note: This field can become stale since it's only set on initial proxy
|
||||
// startup. To get the latest feature flags you'll need to ping from the
|
||||
// auth server.
|
||||
// https://github.com/gravitational/teleport/issues/39161
|
||||
ClusterFeatures proto.Features
|
||||
// clusterFeatures contain flags for supported and unsupported features.
|
||||
clusterFeatures proto.Features
|
||||
|
||||
// nodeWatcher is a services.NodeWatcher used by Assist to lookup nodes from
|
||||
// the proxy's cache and get nodes in real time.
|
||||
|
@ -314,6 +314,10 @@ type Config struct {
|
|||
|
||||
// IntegrationAppHandler handles App Access requests which use an Integration.
|
||||
IntegrationAppHandler app.ServerHandler
|
||||
|
||||
// FeatureWatchInterval is the interval between pings to the auth server
|
||||
// to fetch new cluster features
|
||||
FeatureWatchInterval time.Duration
|
||||
}
|
||||
|
||||
// SetDefaults ensures proper default values are set if
|
||||
|
@ -328,6 +332,8 @@ func (c *Config) SetDefaults() {
|
|||
if c.PresenceChecker == nil {
|
||||
c.PresenceChecker = client.RunPresenceTask
|
||||
}
|
||||
|
||||
c.FeatureWatchInterval = cmp.Or(c.FeatureWatchInterval, DefaultFeatureWatchInterval)
|
||||
}
|
||||
|
||||
type APIHandler struct {
|
||||
|
@ -451,7 +457,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
|||
log: newPackageLogger(),
|
||||
logger: slog.Default().With(teleport.ComponentKey, teleport.ComponentWeb),
|
||||
clock: clockwork.NewRealClock(),
|
||||
ClusterFeatures: cfg.ClusterFeatures,
|
||||
clusterFeatures: cfg.ClusterFeatures,
|
||||
healthCheckAppServer: cfg.HealthCheckAppServer,
|
||||
tracer: cfg.TracerProvider.Tracer(teleport.ComponentWeb),
|
||||
wsIODeadline: wsIODeadline,
|
||||
|
@ -682,6 +688,8 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) {
|
|||
}
|
||||
}
|
||||
|
||||
go h.startFeatureWatcher()
|
||||
|
||||
return &APIHandler{
|
||||
handler: h,
|
||||
appHandler: appHandler,
|
||||
|
@ -1164,17 +1172,12 @@ func (h *Handler) getUserContext(w http.ResponseWriter, r *http.Request, p httpr
|
|||
}
|
||||
desktopRecordingEnabled := recConfig.GetMode() != types.RecordOff
|
||||
|
||||
pingResp, err := clt.Ping(r.Context())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
features := pingResp.GetServerFeatures()
|
||||
entitlement := modules.GetProtoEntitlement(features, entitlements.AccessMonitoring)
|
||||
features := h.GetClusterFeatures()
|
||||
entitlement := modules.GetProtoEntitlement(&features, entitlements.AccessMonitoring)
|
||||
// ensure entitlement is set & feature is configured
|
||||
accessMonitoringEnabled := entitlement.Enabled && features.GetAccessMonitoringConfigured()
|
||||
|
||||
userContext, err := ui.NewUserContext(user, accessChecker.Roles(), *pingResp.ServerFeatures, desktopRecordingEnabled, accessMonitoringEnabled)
|
||||
userContext, err := ui.NewUserContext(user, accessChecker.Roles(), features, desktopRecordingEnabled, accessMonitoringEnabled)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
@ -1692,14 +1695,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou
|
|||
}
|
||||
}
|
||||
|
||||
clusterFeatures := h.ClusterFeatures
|
||||
// ping server to get cluster features since h.ClusterFeatures may be stale
|
||||
pingResponse, err := h.GetProxyClient().Ping(r.Context())
|
||||
if err != nil {
|
||||
h.log.WithError(err).Warn("Cannot retrieve cluster features, client may receive stale features")
|
||||
} else {
|
||||
clusterFeatures = *pingResponse.ServerFeatures
|
||||
}
|
||||
clusterFeatures := h.GetClusterFeatures()
|
||||
|
||||
// get tunnel address to display on cloud instances
|
||||
tunnelPublicAddr := ""
|
||||
|
@ -1813,7 +1809,6 @@ func setEntitlementsWithLegacyLogic(webCfg *webclient.WebConfig, clusterFeatures
|
|||
webCfg.Entitlements[string(entitlements.OIDC)] = webclient.EntitlementInfo{Enabled: clusterFeatures.GetOIDC()}
|
||||
webCfg.Entitlements[string(entitlements.Policy)] = webclient.EntitlementInfo{Enabled: clusterFeatures.GetPolicy() != nil && clusterFeatures.GetPolicy().Enabled}
|
||||
webCfg.Entitlements[string(entitlements.SAML)] = webclient.EntitlementInfo{Enabled: clusterFeatures.GetSAML()}
|
||||
|
||||
// set default Identity fields to legacy feature value
|
||||
webCfg.Entitlements[string(entitlements.AccessLists)] = webclient.EntitlementInfo{Enabled: true, Limit: clusterFeatures.GetAccessList().GetCreateLimit()}
|
||||
webCfg.Entitlements[string(entitlements.AccessMonitoring)] = webclient.EntitlementInfo{Enabled: clusterFeatures.GetAccessMonitoring().GetEnabled(), Limit: clusterFeatures.GetAccessMonitoring().GetMaxReportRangeLimit()}
|
||||
|
|
|
@ -4561,6 +4561,7 @@ func TestApplicationWebSessionsDeletedAfterLogout(t *testing.T) {
|
|||
func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
env := newWebPack(t, 1)
|
||||
handler := env.proxies[0].handler.handler
|
||||
|
||||
// Set auth preference with passwordless.
|
||||
const MOTD = "Welcome to cluster, your activity will be recorded."
|
||||
|
@ -4591,6 +4592,9 @@ func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
|||
_, err = env.server.Auth().UpsertGithubConnector(ctx, github)
|
||||
require.NoError(t, err)
|
||||
|
||||
// start the feature watcher so the web config gets new features
|
||||
env.clock.Advance(DefaultFeatureWatchInterval * 2)
|
||||
|
||||
expectedCfg := webclient.WebConfig{
|
||||
Auth: webclient.WebConfigAuthSettings{
|
||||
SecondFactor: constants.SecondFactorOptional,
|
||||
|
@ -4680,6 +4684,7 @@ func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
|||
},
|
||||
},
|
||||
})
|
||||
env.clock.Advance(DefaultFeatureWatchInterval * 2)
|
||||
|
||||
require.NoError(t, err)
|
||||
// This version is too high and MUST NOT be used
|
||||
|
@ -4690,7 +4695,7 @@ func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
|||
},
|
||||
}
|
||||
require.NoError(t, channels.CheckAndSetDefaults())
|
||||
env.proxies[0].handler.handler.cfg.AutomaticUpgradesChannels = channels
|
||||
handler.cfg.AutomaticUpgradesChannels = channels
|
||||
|
||||
expectedCfg.IsCloud = true
|
||||
expectedCfg.IsUsageBasedBilling = true
|
||||
|
@ -4706,14 +4711,20 @@ func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
|||
expectedCfg.Entitlements[string(entitlements.JoinActiveSessions)] = webclient.EntitlementInfo{Enabled: false}
|
||||
expectedCfg.Entitlements[string(entitlements.K8s)] = webclient.EntitlementInfo{Enabled: false}
|
||||
|
||||
// request and verify enabled features are enabled.
|
||||
re, err = clt.Get(ctx, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(re.Bytes()), "var GRV_CONFIG"))
|
||||
str = strings.ReplaceAll(string(re.Bytes()), "var GRV_CONFIG = ", "")
|
||||
err = json.Unmarshal([]byte(str[:len(str)-1]), &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCfg, cfg)
|
||||
// request and verify enabled features are eventually enabled.
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
re, err := clt.Get(ctx, endpoint, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.True(t, bytes.HasPrefix(re.Bytes(), []byte("var GRV_CONFIG")))
|
||||
res := bytes.ReplaceAll(re.Bytes(), []byte("var GRV_CONFIG = "), []byte{})
|
||||
err = json.Unmarshal(res[:len(res)-1], &cfg)
|
||||
assert.NoError(t, err)
|
||||
diff := cmp.Diff(expectedCfg, cfg)
|
||||
assert.Empty(t, diff)
|
||||
|
||||
}, time.Second*5, time.Millisecond*50)
|
||||
|
||||
// use mock client to assert that if ping returns an error, we'll default to
|
||||
// cluster config
|
||||
|
@ -4736,15 +4747,22 @@ func TestGetWebConfig_WithEntitlements(t *testing.T) {
|
|||
IsUsageBasedBilling: false,
|
||||
},
|
||||
})
|
||||
env.clock.Advance(DefaultFeatureWatchInterval * 2)
|
||||
|
||||
// request and verify again
|
||||
re, err = clt.Get(ctx, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(re.Bytes()), "var GRV_CONFIG"))
|
||||
str = strings.ReplaceAll(string(re.Bytes()), "var GRV_CONFIG = ", "")
|
||||
err = json.Unmarshal([]byte(str[:len(str)-1]), &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCfg, cfg)
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
re, err := clt.Get(ctx, endpoint, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.True(t, bytes.HasPrefix(re.Bytes(), []byte("var GRV_CONFIG")))
|
||||
res := bytes.ReplaceAll(re.Bytes(), []byte("var GRV_CONFIG = "), []byte{})
|
||||
err = json.Unmarshal(res[:len(res)-1], &cfg)
|
||||
assert.NoError(t, err)
|
||||
diff := cmp.Diff(expectedCfg, cfg)
|
||||
assert.Empty(t, diff)
|
||||
|
||||
}, time.Second*5, time.Millisecond*50)
|
||||
}
|
||||
|
||||
func TestGetWebConfig_LegacyFeatureLimits(t *testing.T) {
|
||||
|
@ -4764,6 +4782,8 @@ func TestGetWebConfig_LegacyFeatureLimits(t *testing.T) {
|
|||
},
|
||||
},
|
||||
})
|
||||
// start the feature watcher so the web config gets new features
|
||||
env.clock.Advance(DefaultFeatureWatchInterval * 2)
|
||||
|
||||
expectedCfg := webclient.WebConfig{
|
||||
Auth: webclient.WebConfigAuthSettings{
|
||||
|
@ -4812,20 +4832,25 @@ func TestGetWebConfig_LegacyFeatureLimits(t *testing.T) {
|
|||
PlayableDatabaseProtocols: player.SupportedDatabaseProtocols,
|
||||
}
|
||||
|
||||
// Make a request.
|
||||
clt := env.proxies[0].newClient(t)
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
// Make a request.
|
||||
endpoint := clt.Endpoint("web", "config.js")
|
||||
re, err := clt.Get(ctx, endpoint, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasPrefix(string(re.Bytes()), "var GRV_CONFIG"))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.True(t, bytes.HasPrefix(re.Bytes(), []byte("var GRV_CONFIG")))
|
||||
|
||||
// Response is type application/javascript, we need to strip off the variable name
|
||||
// and the semicolon at the end, then we are left with json like object.
|
||||
var cfg webclient.WebConfig
|
||||
str := strings.ReplaceAll(string(re.Bytes()), "var GRV_CONFIG = ", "")
|
||||
err = json.Unmarshal([]byte(str[:len(str)-1]), &cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCfg, cfg)
|
||||
res := bytes.ReplaceAll(re.Bytes(), []byte("var GRV_CONFIG = "), []byte{})
|
||||
err = json.Unmarshal(res[:len(res)-1], &cfg)
|
||||
assert.NoError(t, err)
|
||||
diff := cmp.Diff(expectedCfg, cfg)
|
||||
assert.Empty(t, diff)
|
||||
}, time.Second*5, time.Millisecond*50)
|
||||
}
|
||||
|
||||
func TestCreatePrivilegeToken(t *testing.T) {
|
||||
|
|
73
lib/web/features.go
Normal file
73
lib/web/features.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2024 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 web
|
||||
|
||||
import (
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/entitlements"
|
||||
)
|
||||
|
||||
// SetClusterFeatures sets the flags for supported and unsupported features.
|
||||
// TODO(mcbattirola): make method unexported, fix tests using it to set
|
||||
// test modules instead.
|
||||
func (h *Handler) SetClusterFeatures(features proto.Features) {
|
||||
h.Mutex.Lock()
|
||||
defer h.Mutex.Unlock()
|
||||
|
||||
entitlements.BackfillFeatures(&features)
|
||||
h.clusterFeatures = features
|
||||
}
|
||||
|
||||
// GetClusterFeatures returns flags for supported and unsupported features.
|
||||
func (h *Handler) GetClusterFeatures() proto.Features {
|
||||
h.Mutex.Lock()
|
||||
defer h.Mutex.Unlock()
|
||||
|
||||
return h.clusterFeatures
|
||||
}
|
||||
|
||||
// startFeatureWatcher periodically pings the auth server and updates `clusterFeatures`.
|
||||
// Must be called only once per `handler`, otherwise it may close an already closed channel
|
||||
// which will cause a panic.
|
||||
// The watcher doesn't ping the auth server immediately upon start because features are
|
||||
// already set by the config object in `NewHandler`.
|
||||
func (h *Handler) startFeatureWatcher() {
|
||||
ticker := h.clock.NewTicker(h.cfg.FeatureWatchInterval)
|
||||
h.log.WithField("interval", h.cfg.FeatureWatchInterval).Info("Proxy handler features watcher has started")
|
||||
ctx := h.cfg.Context
|
||||
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.Chan():
|
||||
h.log.Info("Pinging auth server for features")
|
||||
pingResponse, err := h.GetProxyClient().Ping(ctx)
|
||||
if err != nil {
|
||||
h.log.WithError(err).Error("Auth server ping failed")
|
||||
continue
|
||||
}
|
||||
|
||||
h.SetClusterFeatures(*pingResponse.ServerFeatures)
|
||||
h.log.WithField("features", pingResponse.ServerFeatures).Info("Done updating proxy features")
|
||||
case <-ctx.Done():
|
||||
h.log.Info("Feature service has stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
176
lib/web/features_test.go
Normal file
176
lib/web/features_test.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2024 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 web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/jonboulle/clockwork"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/entitlements"
|
||||
"github.com/gravitational/teleport/lib/auth/authclient"
|
||||
)
|
||||
|
||||
// mockedPingTestProxy is a test proxy with a mocked Ping method
|
||||
// that returns the internal features
|
||||
type mockedFeatureGetter struct {
|
||||
authclient.ClientI
|
||||
features proto.Features
|
||||
}
|
||||
|
||||
func (m *mockedFeatureGetter) Ping(ctx context.Context) (proto.PingResponse, error) {
|
||||
return proto.PingResponse{
|
||||
ServerFeatures: utils.CloneProtoMsg(&m.features),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockedFeatureGetter) setFeatures(f proto.Features) {
|
||||
m.features = f
|
||||
}
|
||||
|
||||
func TestFeaturesWatcher(t *testing.T) {
|
||||
clock := clockwork.NewFakeClock()
|
||||
|
||||
mockClient := &mockedFeatureGetter{features: proto.Features{
|
||||
Kubernetes: true,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
handler := &Handler{
|
||||
cfg: Config{
|
||||
FeatureWatchInterval: 100 * time.Millisecond,
|
||||
ProxyClient: mockClient,
|
||||
Context: ctx,
|
||||
},
|
||||
clock: clock,
|
||||
clusterFeatures: proto.Features{},
|
||||
log: newPackageLogger(),
|
||||
logger: slog.Default().With(teleport.ComponentKey, teleport.ComponentWeb),
|
||||
}
|
||||
|
||||
// before running the watcher, features should match the value passed to the handler
|
||||
requireFeatures(t, clock, proto.Features{}, handler.GetClusterFeatures)
|
||||
|
||||
go handler.startFeatureWatcher()
|
||||
clock.BlockUntil(1)
|
||||
|
||||
// after starting the watcher, handler.GetClusterFeatures should return
|
||||
// values matching the client's response
|
||||
features := proto.Features{
|
||||
Kubernetes: true,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}
|
||||
entitlements.BackfillFeatures(&features)
|
||||
expected := utils.CloneProtoMsg(&features)
|
||||
requireFeatures(t, clock, *expected, handler.GetClusterFeatures)
|
||||
|
||||
// update values once again and check if the features are properly updated
|
||||
features = proto.Features{
|
||||
Kubernetes: false,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}
|
||||
entitlements.BackfillFeatures(&features)
|
||||
mockClient.setFeatures(features)
|
||||
expected = utils.CloneProtoMsg(&features)
|
||||
requireFeatures(t, clock, *expected, handler.GetClusterFeatures)
|
||||
|
||||
// test updating entitlements
|
||||
features = proto.Features{
|
||||
Kubernetes: true,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.AccessLists): {Enabled: true},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true},
|
||||
string(entitlements.App): {Enabled: true},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: true},
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}
|
||||
entitlements.BackfillFeatures(&features)
|
||||
mockClient.setFeatures(features)
|
||||
|
||||
expected = &proto.Features{
|
||||
Kubernetes: true,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{
|
||||
string(entitlements.ExternalAuditStorage): {Enabled: true},
|
||||
string(entitlements.AccessLists): {Enabled: true},
|
||||
string(entitlements.AccessMonitoring): {Enabled: true},
|
||||
string(entitlements.App): {Enabled: true},
|
||||
string(entitlements.CloudAuditLogRetention): {Enabled: true},
|
||||
},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}
|
||||
entitlements.BackfillFeatures(expected)
|
||||
requireFeatures(t, clock, *expected, handler.GetClusterFeatures)
|
||||
|
||||
// stop watcher and ensure it stops updating features
|
||||
cancel()
|
||||
features = proto.Features{
|
||||
Kubernetes: !features.Kubernetes,
|
||||
App: !features.App,
|
||||
DB: true,
|
||||
Entitlements: map[string]*proto.EntitlementInfo{},
|
||||
AccessRequests: &proto.AccessRequestsFeature{},
|
||||
}
|
||||
entitlements.BackfillFeatures(&features)
|
||||
mockClient.setFeatures(features)
|
||||
notExpected := utils.CloneProtoMsg(&features)
|
||||
// assert the handler never get these last features as the watcher is stopped
|
||||
neverFeatures(t, clock, *notExpected, handler.GetClusterFeatures)
|
||||
}
|
||||
|
||||
// requireFeatures is a helper function that advances the clock, then
|
||||
// calls `getFeatures` every 100ms for up to 1 second, until it
|
||||
// returns the expected result (`want`).
|
||||
func requireFeatures(t *testing.T, fakeClock clockwork.FakeClock, want proto.Features, getFeatures func() proto.Features) {
|
||||
t.Helper()
|
||||
|
||||
// Advance the clock so the service fetch and stores features
|
||||
fakeClock.Advance(1 * time.Second)
|
||||
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
diff := cmp.Diff(want, getFeatures())
|
||||
assert.Empty(t, diff)
|
||||
}, 5*time.Second, time.Millisecond*100)
|
||||
}
|
||||
|
||||
// neverFeatures is a helper function that advances the clock, then
|
||||
// calls `getFeatures` every 100ms for up to 1 second. If at some point `getFeatures`
|
||||
// returns `doNotWant`, the test fails.
|
||||
func neverFeatures(t *testing.T, fakeClock clockwork.FakeClock, doNotWant proto.Features, getFeatures func() proto.Features) {
|
||||
t.Helper()
|
||||
|
||||
fakeClock.Advance(1 * time.Second)
|
||||
require.Never(t, func() bool {
|
||||
return cmp.Diff(doNotWant, getFeatures()) == ""
|
||||
}, 1*time.Second, time.Millisecond*100)
|
||||
}
|
|
@ -148,7 +148,7 @@ func (h *Handler) awsOIDCDeployService(w http.ResponseWriter, r *http.Request, p
|
|||
}
|
||||
|
||||
teleportVersionTag := teleport.Version
|
||||
if automaticUpgrades(h.ClusterFeatures) {
|
||||
if automaticUpgrades(h.GetClusterFeatures()) {
|
||||
cloudStableVersion, err := h.cfg.AutomaticUpgradesChannels.DefaultVersion(ctx)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
|
@ -201,7 +201,7 @@ func (h *Handler) awsOIDCDeployDatabaseServices(w http.ResponseWriter, r *http.R
|
|||
}
|
||||
|
||||
teleportVersionTag := teleport.Version
|
||||
if automaticUpgrades(h.ClusterFeatures) {
|
||||
if automaticUpgrades(h.GetClusterFeatures()) {
|
||||
cloudStableVersion, err := h.cfg.AutomaticUpgradesChannels.DefaultVersion(ctx)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
|
@ -527,7 +527,7 @@ func (h *Handler) awsOIDCEnrollEKSClusters(w http.ResponseWriter, r *http.Reques
|
|||
return nil, trace.BadParameter("an integration name is required")
|
||||
}
|
||||
|
||||
agentVersion, err := kubeutils.GetKubeAgentVersion(ctx, h.cfg.ProxyClient, h.ClusterFeatures, h.cfg.AutomaticUpgradesChannels)
|
||||
agentVersion, err := kubeutils.GetKubeAgentVersion(ctx, h.cfg.ProxyClient, h.GetClusterFeatures(), h.cfg.AutomaticUpgradesChannels)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
|
|
@ -377,7 +377,7 @@ func (h *Handler) createTokenForDiscoveryHandle(w http.ResponseWriter, r *http.R
|
|||
func (h *Handler) getAutoUpgrades(ctx context.Context) (bool, string, error) {
|
||||
var autoUpgradesVersion string
|
||||
var err error
|
||||
autoUpgrades := automaticUpgrades(h.ClusterFeatures)
|
||||
autoUpgrades := automaticUpgrades(h.GetClusterFeatures())
|
||||
if autoUpgrades {
|
||||
autoUpgradesVersion, err = h.cfg.AutomaticUpgradesChannels.DefaultVersion(ctx)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in a new issue