From 87970ea9c1dd797788b9bb6c306d912483957714 Mon Sep 17 00:00:00 2001 From: Michelle Bergquist <11967646+michellescripts@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:22:01 -0600 Subject: [PATCH] Set entitlements when unsupported by auth (#45226) --- lib/service/connect.go | 71 ++++++++- lib/service/connect_test.go | 283 ++++++++++++++++++++++++++++++++++++ 2 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 lib/service/connect_test.go diff --git a/lib/service/connect.go b/lib/service/connect.go index 8d1edfde555ae..ce377b68c78e7 100644 --- a/lib/service/connect.go +++ b/lib/service/connect.go @@ -43,7 +43,9 @@ import ( "github.com/gravitational/teleport/api/client/proto" apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" + apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/api/utils/retryutils" + "github.com/gravitational/teleport/entitlements" "github.com/gravitational/teleport/lib" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/authclient" @@ -1135,7 +1137,10 @@ func (process *TeleportProcess) getConnector(clientIdentity, serverIdentity *sta } // Set cluster features and return successfully with a working connector. - process.setClusterFeatures(pingResponse.GetServerFeatures()) + // TODO(michellescripts) remove clone & compatibility check in v18 + cloned := apiutils.CloneProtoMsg(pingResponse.GetServerFeatures()) + supportEntitlementsCompatibility(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()) @@ -1146,6 +1151,70 @@ func (process *TeleportProcess) getConnector(clientIdentity, serverIdentity *sta }, 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. diff --git a/lib/service/connect_test.go b/lib/service/connect_test.go new file mode 100644 index 0000000000000..5a2df2e136ef5 --- /dev/null +++ b/lib/service/connect_test.go @@ -0,0 +1,283 @@ +// 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 . + +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}, + }, + }, + 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}, + }, + }, + { + 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}, + }, + }, + { + 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}, + // 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) + }) + } +}