Skip to content
52 changes: 51 additions & 1 deletion src/operator/webhooks/clientintents_webhook_v2alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"fmt"
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
"github.com/spf13/viper"
"golang.org/x/net/idna"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -64,6 +67,13 @@ func (v *IntentsValidatorV2alpha1) ValidateCreate(ctx context.Context, obj runti
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
return nil, errors.Wrap(err)
}

if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
allErrs = append(allErrs, err)
}
}

if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
allErrs = append(allErrs, err)
}
Expand All @@ -90,6 +100,13 @@ func (v *IntentsValidatorV2alpha1) ValidateUpdate(ctx context.Context, oldObj, n
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
return nil, errors.Wrap(err)
}

if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
allErrs = append(allErrs, err)
}
}

if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
allErrs = append(allErrs, err)
}
Expand Down Expand Up @@ -402,7 +419,6 @@ func (v *IntentsValidatorV2alpha1) validateAzureTarget(azureTarget *otterizev2al
Detail: "invalid intent format, if actions or dataActions are set, roles must be empty",
}
}

return nil

}
Expand Down Expand Up @@ -464,3 +480,37 @@ func (v *IntentsValidatorV2alpha1) validateInternetTarget(internetTarget *otteri
}
return nil
}

func (v *IntentsValidatorV2alpha1) enforceIntentsAbideStrictMode(intents *otterizev2alpha1.ClientIntents) *field.Error {
for _, target := range intents.GetTargetList() {
intentType := target.GetIntentType()
switch intentType {
case otterizev2alpha1.IntentTypeInternet:
if hasWildcardDomain(target.Internet.Domains) {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "domains",
Detail: fmt.Sprintf("invalid target format. type %s must not contain wildcard domains while in strict mode", intentType),
}
}
if len(target.Internet.Ports) == 0 {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "ports",
Detail: fmt.Sprintf("invalid target format. Type %s must contain ports while in strict mode", intentType),
}
}
case otterizev2alpha1.IntentTypeHTTP, "": // Empty type is also considered HTTP
if target.Service == nil && (target.Kubernetes == nil || target.Kubernetes.Kind != serviceidentity.KindService) {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "service",
Detail: fmt.Sprint("invalid target format. Target must be a Kubernetes service while in strict mode"),
}
}
default:
continue
}
}
return nil
}
51 changes: 51 additions & 0 deletions src/operator/webhooks/clientintents_webhook_v2beta1.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
"fmt"
otterizev2beta1 "github.com/otterize/intents-operator/src/operator/api/v2beta1"
"github.com/otterize/intents-operator/src/shared/errors"
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
"github.com/otterize/intents-operator/src/shared/serviceidresolver/serviceidentity"
"github.com/spf13/viper"
"golang.org/x/net/idna"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -64,6 +67,13 @@ func (v *IntentsValidatorV2beta1) ValidateCreate(ctx context.Context, obj runtim
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
return nil, errors.Wrap(err)
}

if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
allErrs = append(allErrs, err)
}
}

if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
allErrs = append(allErrs, err)
}
Expand All @@ -90,6 +100,13 @@ func (v *IntentsValidatorV2beta1) ValidateUpdate(ctx context.Context, oldObj, ne
if err := v.List(ctx, intentsList, &client.ListOptions{Namespace: intentsObj.Namespace}); err != nil {
return nil, errors.Wrap(err)
}

if viper.GetBool(enforcement.EnableStrictModeIntentsKey) {
if err := v.enforceIntentsAbideStrictMode(intentsObj); err != nil {
allErrs = append(allErrs, err)
}
}

if err := v.validateNoDuplicateClients(intentsObj, intentsList); err != nil {
allErrs = append(allErrs, err)
}
Expand Down Expand Up @@ -464,3 +481,37 @@ func (v *IntentsValidatorV2beta1) validateInternetTarget(internetTarget *otteriz
}
return nil
}

func (v *IntentsValidatorV2beta1) enforceIntentsAbideStrictMode(intents *otterizev2beta1.ClientIntents) *field.Error {
for _, target := range intents.GetTargetList() {
intentType := target.GetIntentType()
switch intentType {
case otterizev2beta1.IntentTypeInternet:
if hasWildcardDomain(target.Internet.Domains) {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "domains",
Detail: fmt.Sprintf("invalid target format. Type %s must not contain wildcard domains while in strict mode", intentType),
}
}
if len(target.Internet.Ports) == 0 {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "ports",
Detail: fmt.Sprintf("invalid target format. Type %s must contain ports while in strict mode", intentType),
}
}
case otterizev2beta1.IntentTypeHTTP, "": // Empty type is also considered HTTP
if target.Service == nil && (target.Kubernetes == nil || target.Kubernetes.Kind != serviceidentity.KindService) {
return &field.Error{
Type: field.ErrorTypeForbidden,
Field: "service",
Detail: fmt.Sprint("invalid target format. Target must be a Kubernetes service while in strict mode"),
}
}
default:
continue
}
}
return nil
}
15 changes: 15 additions & 0 deletions src/operator/webhooks/strict_mode_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package webhooks

import (
"strings"
)

// hasWildcardDomain checks if a list of FQDNs contains one with a '*' character.
func hasWildcardDomain(fqdns []string) bool {
for _, fqdn := range fqdns {
if strings.Contains(fqdn, "*") {
return true
}
}
return false
}
43 changes: 43 additions & 0 deletions src/operator/webhooks/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ import (
otterizev1beta1 "github.com/otterize/intents-operator/src/operator/api/v1beta1"
otterizev2alpha1 "github.com/otterize/intents-operator/src/operator/api/v2alpha1"
otterizev2beta1 "github.com/otterize/intents-operator/src/operator/api/v2beta1"
"github.com/otterize/intents-operator/src/shared/operatorconfig/enforcement"
"github.com/otterize/intents-operator/src/shared/testbase"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/stretchr/testify/suite"
istiosecurityscheme "istio.io/client-go/pkg/apis/security/v1beta1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand Down Expand Up @@ -771,6 +773,47 @@ func (s *ConversionWebhookTestSuite) TestConversionWebhookInternetIntents() {
}
}

func (s *ValidationWebhookTestSuite) TestStrictModeNonKubernetesServiceRejected() {
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
{
Kubernetes: &otterizev2beta1.KubernetesTarget{
Name: "deny-me",
Kind: "ReplicaSet",
},
},
})
s.Require().Error(err)
s.Require().ErrorContains(err, "Target must be a Kubernetes service while in strict mode")
}

func (s *ValidationWebhookTestSuite) TestStrictModeWildcardDNSRejected() {
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
{
Internet: &otterizev2beta1.Internet{
Domains: []string{"*.example.com"},
},
},
})
s.Require().Error(err)
s.Require().ErrorContains(err, "must not contain wildcard domains while in strict mode")
}

func (s *ValidationWebhookTestSuite) TestStrictModeDNSWithoutPortRejected() {
viper.Set(enforcement.EnableStrictModeIntentsKey, true)
_, err := s.AddIntentsInNamespaceV2beta1("test-intents", "test-client", s.TestNamespace, []otterizev2beta1.Target{
{
Internet: &otterizev2beta1.Internet{
Domains: []string{"api.example.com"},
// Ports is not set
},
},
})
s.Require().Error(err)
s.Require().ErrorContains(err, "must contain ports while in strict mode")
}

func TestValidationWebhookTestSuite(t *testing.T) {
suite.Run(t, new(ValidationWebhookTestSuite))
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/operator_cloud_client/status_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func uploadConfiguration(ctx context.Context, client CloudClient, mgr manager.Ma
LinkerdPolicyEnforcementEnabled: enforcementConfig.EnableLinkerdPolicies && isLinkerdInstalled,
ProtectedServicesEnabled: enforcementConfig.EnableNetworkPolicy, // in this version, protected services are enabled if network policy creation is enabled, regardless of enforcement default state
EnforcedNamespaces: enforcementConfig.EnforcedNamespaces.Items(),
StrictModeEnabled: enforcementConfig.StrictModeEnabled,
ExcludedStrictModeNamespaces: enforcementConfig.ExcludedStrictModeNamespaces.Items(),
AllowExternalTrafficPolicy: getAllowExternalTrafficConfig(), // The server expect for AllowExternalTrafficPolicy because of backwards compatibility
AutomateThirdPartyNetworkPolicies: getAutomateThirdPartyNetworkPoliciesConfig(),
PrometheusServerConfigs: getPrometheusServiceIdentities(),
Expand Down
10 changes: 10 additions & 0 deletions src/shared/operatorconfig/enforcement/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type Config struct {
EnableGCPPolicy bool
EnableAzurePolicy bool
EnableLinkerdPolicies bool
StrictModeEnabled bool
EnforcedNamespaces *goset.Set[string]
ExcludedStrictModeNamespaces *goset.Set[string]
AutomateThirdPartyNetworkPolicies automate_third_party_network_policy.Enum
PrometheusServiceIdentities []serviceidentity.ServiceIdentity
}
Expand Down Expand Up @@ -69,6 +71,9 @@ const (
EnableAzurePolicyKey = "enable-azure-iam-policy"
EnableAzurePolicyDefault = false
PrometheusServiceConfigKey = "prometheusServerConfigs"
EnableStrictModeIntentsKey = "enable-strict-mode-intents" // Whether to enable strict mode intents
EnableStrictModeIntentsDefault = false
ExcludedStrictModeNamespacesKey = "excluded-strict-mode-namespaces"
)

func init() {
Expand All @@ -83,13 +88,16 @@ func init() {
viper.SetDefault(EnableGCPPolicyKey, EnableGCPPolicyDefault)
viper.SetDefault(EnableAzurePolicyKey, EnableAzurePolicyDefault)
viper.SetDefault(AutomateThirdPartyNetworkPoliciesKey, AutomateThirdPartyNetworkPoliciesDefault)
viper.SetDefault(EnableStrictModeIntentsKey, EnableStrictModeIntentsDefault)
}

func InitCLIFlags() {
pflag.Bool(EnforcementDefaultStateKey, EnforcementDefaultStateDefault, "Sets the default state of the enforcement. If true, always enforces. If false, can be overridden using ProtectedService.")
pflag.Bool(EnableNetworkPolicyKey, EnableNetworkPolicyDefault, "Whether to enable Intents network policy creation")
pflag.Bool(EnableKafkaACLKey, EnableKafkaACLDefault, "Whether to disable Intents Kafka ACL creation")
pflag.StringSlice(ActiveEnforcementNamespacesKey, nil, "While using the shadow enforcement mode, namespaces in this list will be treated as if the enforcement were active.")
pflag.StringSlice(ExcludedStrictModeNamespacesKey, nil, "Namespaces to exclude from strict mode intents when it is enabled.")
pflag.Bool(EnableStrictModeIntentsKey, EnableStrictModeIntentsDefault, "Whether to enable strict mode intents")
pflag.Bool(EnableIstioPolicyKey, EnableIstioPolicyDefault, "Whether to enable Istio authorization policy creation")
pflag.Bool(EnableLinkerdPolicyKey, EnableLinkerdPolicyDefault, "Experimental - enable Linkerd policy creation")
pflag.Bool(EnableDatabasePolicy, EnableDatabasePolicyDefault, "Enable the database reconciler")
Expand All @@ -109,7 +117,9 @@ func GetConfig() Config {
EnableAWSPolicy: viper.GetBool(EnableAWSPolicyKey),
EnableGCPPolicy: viper.GetBool(EnableGCPPolicyKey),
EnableAzurePolicy: viper.GetBool(EnableAzurePolicyKey),
StrictModeEnabled: viper.GetBool(EnableStrictModeIntentsKey),
EnforcedNamespaces: goset.FromSlice(viper.GetStringSlice(ActiveEnforcementNamespacesKey)),
ExcludedStrictModeNamespaces: goset.FromSlice(viper.GetStringSlice(ActiveEnforcementNamespacesKey)),
AutomateThirdPartyNetworkPolicies: automate_third_party_network_policy.Enum(viper.GetString(AutomateThirdPartyNetworkPoliciesKey)),
PrometheusServiceIdentities: GetPrometheusServiceIdentities(),
}
Expand Down
Loading
Loading