diff --git a/api/cisco/nx/v1alpha1/aaaconfig_types.go b/api/cisco/nx/v1alpha1/aaaconfig_types.go new file mode 100644 index 00000000..46f3ad92 --- /dev/null +++ b/api/cisco/nx/v1alpha1/aaaconfig_types.go @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" +) + +// +kubebuilder:rbac:groups=nx.cisco.networking.metal.ironcore.dev,resources=aaaconfigs,verbs=get;list;watch + +// AAAConfigSpec defines the desired state of AAAConfig +type AAAConfigSpec struct { + // LoginErrorEnable enables login error messages (NX-OS specific). + // Maps to: aaa authentication login error-enable + // +optional + LoginErrorEnable bool `json:"loginErrorEnable,omitempty"` + + // KeyEncryption specifies the default encryption type for TACACS+ keys. + // +kubebuilder:validation:Enum=Type6;Type7;Clear + // +kubebuilder:default=Type7 + KeyEncryption TACACSKeyEncryption `json:"keyEncryption,omitempty"` + + // RADIUSKeyEncryption specifies the default encryption type for RADIUS server keys. + // +kubebuilder:validation:Enum=Type6;Type7;Clear + // +kubebuilder:default=Type7 + RADIUSKeyEncryption RADIUSKeyEncryption `json:"radiusKeyEncryption,omitempty"` + + // ConsoleAuthentication defines NX-OS console-specific authentication methods. + // Maps to: aaa authentication login console + // +optional + ConsoleAuthentication *NXOSMethodList `json:"consoleAuthentication,omitempty"` + + // ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. + // Maps to: aaa authorization config-commands default + // +optional + ConfigCommandsAuthorization *NXOSMethodList `json:"configCommandsAuthorization,omitempty"` +} + +// TACACSKeyEncryption defines the encryption type for TACACS+ server keys. +// +kubebuilder:validation:Enum=Type6;Type7;Clear +type TACACSKeyEncryption string + +const ( + // TACACSKeyEncryptionType6 uses AES encryption (more secure). + TACACSKeyEncryptionType6 TACACSKeyEncryption = "Type6" + // TACACSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). + TACACSKeyEncryptionType7 TACACSKeyEncryption = "Type7" + // TACACSKeyEncryptionClear sends the key in cleartext. + TACACSKeyEncryptionClear TACACSKeyEncryption = "Clear" +) + +// RADIUSKeyEncryption defines the encryption type for RADIUS server keys. +// +kubebuilder:validation:Enum=Type6;Type7;Clear +type RADIUSKeyEncryption string + +const ( + // RADIUSKeyEncryptionType6 uses AES encryption (more secure). + RADIUSKeyEncryptionType6 RADIUSKeyEncryption = "Type6" + // RADIUSKeyEncryptionType7 uses Cisco Type 7 encryption (reversible). + RADIUSKeyEncryptionType7 RADIUSKeyEncryption = "Type7" + // RADIUSKeyEncryptionClear sends the key in cleartext. + RADIUSKeyEncryptionClear RADIUSKeyEncryption = "Clear" +) + +// NXOSMethodList defines an ordered list of AAA methods for NX-OS specific contexts. +type NXOSMethodList struct { + // Methods is the ordered list of methods. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []NXOSMethod `json:"methods"` +} + +// NXOSMethod represents a single AAA method in an NX-OS context. +type NXOSMethod struct { + // Type is the method type. + // +required + // +kubebuilder:validation:Enum=Group;Local;None + Type string `json:"type"` + + // GroupName is the server group name when Type is Group. + // +optional + // +kubebuilder:validation:MaxLength=63 + GroupName string `json:"groupName,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=aaaconfigs +// +kubebuilder:resource:singular=aaaconfig +// +kubebuilder:resource:shortName=nxaaa + +// AAAConfig is the Schema for the aaaconfigs API +type AAAConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Specification of the desired state of the resource. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +required + Spec AAAConfigSpec `json:"spec"` +} + +// +kubebuilder:object:root=true + +// AAAConfigList contains a list of AAAConfig +type AAAConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AAAConfig `json:"items"` +} + +func init() { + v1alpha1.RegisterAAADependency(GroupVersion.WithKind("AAAConfig")) + SchemeBuilder.Register(&AAAConfig{}, &AAAConfigList{}) +} diff --git a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go index ea2a78b7..f6e33cb0 100644 --- a/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go +++ b/api/cisco/nx/v1alpha1/zz_generated.deepcopy.go @@ -13,6 +13,89 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAConfig) DeepCopyInto(out *AAAConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfig. +func (in *AAAConfig) DeepCopy() *AAAConfig { + if in == nil { + return nil + } + out := new(AAAConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAConfigList) DeepCopyInto(out *AAAConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AAAConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfigList. +func (in *AAAConfigList) DeepCopy() *AAAConfigList { + if in == nil { + return nil + } + out := new(AAAConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAConfigSpec) DeepCopyInto(out *AAAConfigSpec) { + *out = *in + if in.ConsoleAuthentication != nil { + in, out := &in.ConsoleAuthentication, &out.ConsoleAuthentication + *out = new(NXOSMethodList) + (*in).DeepCopyInto(*out) + } + if in.ConfigCommandsAuthorization != nil { + in, out := &in.ConfigCommandsAuthorization, &out.ConfigCommandsAuthorization + *out = new(NXOSMethodList) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAConfigSpec. +func (in *AAAConfigSpec) DeepCopy() *AAAConfigSpec { + if in == nil { + return nil + } + out := new(AAAConfigSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AutoRecovery) DeepCopyInto(out *AutoRecovery) { *out = *in @@ -471,6 +554,41 @@ func (in *ManagementAccessConfigSpec) DeepCopy() *ManagementAccessConfigSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NXOSMethod) DeepCopyInto(out *NXOSMethod) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethod. +func (in *NXOSMethod) DeepCopy() *NXOSMethod { + if in == nil { + return nil + } + out := new(NXOSMethod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NXOSMethodList) DeepCopyInto(out *NXOSMethodList) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]NXOSMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NXOSMethodList. +func (in *NXOSMethodList) DeepCopy() *NXOSMethodList { + if in == nil { + return nil + } + out := new(NXOSMethodList) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkVirtualizationEdgeConfig) DeepCopyInto(out *NetworkVirtualizationEdgeConfig) { *out = *in diff --git a/api/core/v1alpha1/aaa_types.go b/api/core/v1alpha1/aaa_types.go new file mode 100644 index 00000000..098b7d9a --- /dev/null +++ b/api/core/v1alpha1/aaa_types.go @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "sync" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// AAASpec defines the desired state of AAA. +// +// It models the Authentication, Authorization, and Accounting (AAA) configuration on a network device, +// aligned with the OpenConfig system/aaa YANG model. +// +kubebuilder:validation:XValidation:rule="has(self.serverGroups) || has(self.authentication) || has(self.authorization) || has(self.accounting)",message="at least one of serverGroups, authentication, authorization, or accounting must be set" +type AAASpec struct { + // DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. + // Immutable. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="DeviceRef is immutable" + DeviceRef LocalObjectReference `json:"deviceRef"` + + // ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this AAA. + // This reference is used to link the AAA to its provider-specific configuration. + // +optional + ProviderConfigRef *TypedLocalObjectReference `json:"providerConfigRef,omitempty"` + + // ServerGroups is the list of AAA server groups. + // OpenConfig: /system/aaa/server-groups/server-group + // +optional + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=8 + ServerGroups []AAAServerGroup `json:"serverGroups,omitempty"` + + // Authentication defines the AAA authentication method list. + // OpenConfig: /system/aaa/authentication + // +optional + Authentication *AAAAuthentication `json:"authentication,omitempty"` + + // Authorization defines the AAA authorization method list. + // OpenConfig: /system/aaa/authorization + // +optional + Authorization *AAAAuthorization `json:"authorization,omitempty"` + + // Accounting defines the AAA accounting method list. + // OpenConfig: /system/aaa/accounting + // +optional + Accounting *AAAAccounting `json:"accounting,omitempty"` +} + +// AAAServerGroupType defines the protocol type of an AAA server group. +// +kubebuilder:validation:Enum=TACACS;RADIUS +type AAAServerGroupType string + +const ( + // AAAServerGroupTypeTACACS is a TACACS+ server group. + AAAServerGroupTypeTACACS AAAServerGroupType = "TACACS" + // AAAServerGroupTypeRADIUS is a RADIUS server group. + AAAServerGroupTypeRADIUS AAAServerGroupType = "RADIUS" +) + +// AAAServerGroup represents a named group of AAA servers. +// OpenConfig: /system/aaa/server-groups/server-group[name] +// +kubebuilder:validation:XValidation:rule="self.type != 'TACACS' || self.servers.all(s, has(s.tacacs))",message="servers in a TACACS group must have tacacs config" +// +kubebuilder:validation:XValidation:rule="self.type != 'RADIUS' || self.servers.all(s, has(s.radius))",message="servers in a RADIUS group must have radius config" +type AAAServerGroup struct { + // Name is the name of the server group. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + Name string `json:"name"` + + // Type is the protocol type of this server group. + // +required + Type AAAServerGroupType `json:"type"` + + // Servers is the list of servers in this group. + // OpenConfig: /system/aaa/server-groups/server-group/servers/server + // +required + // +listType=map + // +listMapKey=address + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + Servers []AAAServer `json:"servers"` + + // VrfName is the VRF to use for communication with the servers in this group. + // +optional + // +kubebuilder:validation:MaxLength=63 + VrfName string `json:"vrfName,omitempty"` + + // SourceInterfaceName is the source interface to use for communication with the servers. + // +optional + // +kubebuilder:validation:MaxLength=63 + SourceInterfaceName string `json:"sourceInterfaceName,omitempty"` +} + +// AAAServer represents a single AAA server within a group. +// OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] +type AAAServer struct { + // Address is the IP address or hostname of the server. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Address string `json:"address"` + + // Timeout is the response timeout in seconds for this server. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=60 + Timeout *int32 `json:"timeout,omitempty"` + + // TACACS contains TACACS+ specific server configuration. + // Required when the parent server group type is TACACS. + // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs + // +optional + TACACS *AAAServerTACACS `json:"tacacs,omitempty"` + + // RADIUS contains RADIUS specific server configuration. + // Required when the parent server group type is RADIUS. + // OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/radius + // +optional + RADIUS *AAAServerRADIUS `json:"radius,omitempty"` +} + +// AAAServerTACACS contains TACACS+ specific server configuration. +type AAAServerTACACS struct { + // Port is the TCP port of the TACACS+ server. + // Defaults to 49 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=49 + Port int32 `json:"port,omitempty"` + + // KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + // The secret must contain a key specified in the SecretKeySelector. + // +required + KeySecretRef SecretKeySelector `json:"keySecretRef"` +} + +// AAAServerRADIUS contains RADIUS specific server configuration. +type AAAServerRADIUS struct { + // AuthPort is the UDP port for RADIUS authentication requests. + // Defaults to 1812 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=1812 + AuthPort int32 `json:"authPort,omitempty"` + + // AcctPort is the UDP port for RADIUS accounting requests. + // Defaults to 1813 if not specified. + // +optional + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Maximum=65535 + // +kubebuilder:default=1813 + AcctPort int32 `json:"acctPort,omitempty"` + + // KeySecretRef is a reference to a secret containing the shared key for this RADIUS server. + // The secret must contain a key specified in the SecretKeySelector. + // +required + KeySecretRef SecretKeySelector `json:"keySecretRef"` +} + +// AAAAuthentication defines the AAA authentication method list. +// OpenConfig: /system/aaa/authentication +type AAAAuthentication struct { + // Methods is the ordered list of authentication methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` +} + +// AAAAuthorization defines the AAA authorization method list. +// OpenConfig: /system/aaa/authorization +type AAAAuthorization struct { + // Methods is the ordered list of authorization methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` +} + +// AAAAccounting defines the AAA accounting method list. +// OpenConfig: /system/aaa/accounting +type AAAAccounting struct { + // Methods is the ordered list of accounting methods. + // Methods are tried in order until one succeeds or all fail. + // +required + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Methods []AAAMethod `json:"methods"` +} + +// AAAMethod represents an AAA method. +// +kubebuilder:validation:XValidation:rule="self.type != 'Group' || self.groupName != \"\"",message="groupName is required when type is Group" +type AAAMethod struct { + // Type is the type of AAA method. + // +required + // +kubebuilder:validation:Enum=Group;Local;None + Type AAAMethodType `json:"type"` + + // GroupName is the name of the server group when Type is Group. + // +optional + // +kubebuilder:validation:MaxLength=63 + GroupName string `json:"groupName,omitempty"` +} + +// AAAMethodType defines the type of AAA method. +// +kubebuilder:validation:Enum=Group;Local;None +type AAAMethodType string + +const ( + // AAAMethodTypeGroup uses a server group (e.g., TACACS+ group). + AAAMethodTypeGroup AAAMethodType = "Group" + // AAAMethodTypeLocal uses the local user database. + AAAMethodTypeLocal AAAMethodType = "Local" + // AAAMethodTypeNone allows access without authentication. + AAAMethodTypeNone AAAMethodType = "None" +) + +// AAAStatus defines the observed state of AAA. +type AAAStatus struct { + // The conditions are a list of status objects that describe the state of the AAA. + // +listType=map + // +listMapKey=type + // +patchStrategy=merge + // +patchMergeKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=aaas +// +kubebuilder:resource:singular=aaa +// +kubebuilder:resource:shortName=aaa +// +kubebuilder:printcolumn:name="Device",type=string,JSONPath=`.spec.deviceRef.name` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// AAA is the Schema for the aaas API +type AAA struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Specification of the desired state of the resource. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +required + Spec AAASpec `json:"spec"` + + // Status of the resource. This is set and updated automatically. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +optional + Status AAAStatus `json:"status,omitempty,omitzero"` +} + +// GetConditions implements conditions.Getter. +func (a *AAA) GetConditions() []metav1.Condition { + return a.Status.Conditions +} + +// SetConditions implements conditions.Setter. +func (a *AAA) SetConditions(conditions []metav1.Condition) { + a.Status.Conditions = conditions +} + +// +kubebuilder:object:root=true + +// AAAList contains a list of AAA +type AAAList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AAA `json:"items"` +} + +var ( + AAADependencies []schema.GroupVersionKind + aaaDependenciesMu sync.Mutex +) + +func RegisterAAADependency(gvk schema.GroupVersionKind) { + aaaDependenciesMu.Lock() + defer aaaDependenciesMu.Unlock() + AAADependencies = append(AAADependencies, gvk) +} + +func init() { + SchemeBuilder.Register(&AAA{}, &AAAList{}) +} diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 32988e04..422a5a40 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,268 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAA) DeepCopyInto(out *AAA) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAA. +func (in *AAA) DeepCopy() *AAA { + if in == nil { + return nil + } + out := new(AAA) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAA) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAccounting) DeepCopyInto(out *AAAAccounting) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAccounting. +func (in *AAAAccounting) DeepCopy() *AAAAccounting { + if in == nil { + return nil + } + out := new(AAAAccounting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthentication) DeepCopyInto(out *AAAAuthentication) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthentication. +func (in *AAAAuthentication) DeepCopy() *AAAAuthentication { + if in == nil { + return nil + } + out := new(AAAAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAAuthorization) DeepCopyInto(out *AAAAuthorization) { + *out = *in + if in.Methods != nil { + in, out := &in.Methods, &out.Methods + *out = make([]AAAMethod, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAAuthorization. +func (in *AAAAuthorization) DeepCopy() *AAAAuthorization { + if in == nil { + return nil + } + out := new(AAAAuthorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAList) DeepCopyInto(out *AAAList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AAA, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAList. +func (in *AAAList) DeepCopy() *AAAList { + if in == nil { + return nil + } + out := new(AAAList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AAAList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAMethod) DeepCopyInto(out *AAAMethod) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAMethod. +func (in *AAAMethod) DeepCopy() *AAAMethod { + if in == nil { + return nil + } + out := new(AAAMethod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServer) DeepCopyInto(out *AAAServer) { + *out = *in + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(int32) + **out = **in + } + if in.TACACS != nil { + in, out := &in.TACACS, &out.TACACS + *out = new(AAAServerTACACS) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServer. +func (in *AAAServer) DeepCopy() *AAAServer { + if in == nil { + return nil + } + out := new(AAAServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServerGroup) DeepCopyInto(out *AAAServerGroup) { + *out = *in + if in.Servers != nil { + in, out := &in.Servers, &out.Servers + *out = make([]AAAServer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServerGroup. +func (in *AAAServerGroup) DeepCopy() *AAAServerGroup { + if in == nil { + return nil + } + out := new(AAAServerGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAServerTACACS) DeepCopyInto(out *AAAServerTACACS) { + *out = *in + out.KeySecretRef = in.KeySecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAServerTACACS. +func (in *AAAServerTACACS) DeepCopy() *AAAServerTACACS { + if in == nil { + return nil + } + out := new(AAAServerTACACS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAASpec) DeepCopyInto(out *AAASpec) { + *out = *in + out.DeviceRef = in.DeviceRef + if in.ProviderConfigRef != nil { + in, out := &in.ProviderConfigRef, &out.ProviderConfigRef + *out = new(TypedLocalObjectReference) + **out = **in + } + if in.ServerGroups != nil { + in, out := &in.ServerGroups, &out.ServerGroups + *out = make([]AAAServerGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(AAAAuthentication) + (*in).DeepCopyInto(*out) + } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(AAAAuthorization) + (*in).DeepCopyInto(*out) + } + if in.Accounting != nil { + in, out := &in.Accounting, &out.Accounting + *out = new(AAAAccounting) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAASpec. +func (in *AAASpec) DeepCopy() *AAASpec { + if in == nil { + return nil + } + out := new(AAASpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AAAStatus) DeepCopyInto(out *AAAStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AAAStatus. +func (in *AAAStatus) DeepCopy() *AAAStatus { + if in == nil { + return nil + } + out := new(AAAStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ACLEntry) DeepCopyInto(out *ACLEntry) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 2b375426..64fe7347 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -559,6 +559,18 @@ func main() { os.Exit(1) } + if err := (&corecontroller.AAAReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("aaa-controller"), + WatchFilterValue: watchFilterValue, + Provider: prov, + Locker: locker, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AAA") + os.Exit(1) + } + if err := (&corecontroller.PrefixSetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml new file mode 100644 index 00000000..ced5f156 --- /dev/null +++ b/config/crd/bases/networking.metal.ironcore.dev_aaas.yaml @@ -0,0 +1,439 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: aaas.networking.metal.ironcore.dev +spec: + group: networking.metal.ironcore.dev + names: + kind: AAA + listKind: AAAList + plural: aaas + shortNames: + - aaa + singular: aaa + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.deviceRef.name + name: Device + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: AAA is the Schema for the aaas API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + accounting: + description: |- + Accounting defines the AAA accounting method list. + OpenConfig: /system/aaa/accounting + properties: + methods: + description: |- + Methods is the ordered list of accounting methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + authentication: + description: |- + Authentication defines the AAA authentication method list. + OpenConfig: /system/aaa/authentication + properties: + methods: + description: |- + Methods is the ordered list of authentication methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + authorization: + description: |- + Authorization defines the AAA authorization method list. + OpenConfig: /system/aaa/authorization + properties: + methods: + description: |- + Methods is the ordered list of authorization methods. + Methods are tried in order until one succeeds or all fail. + items: + description: AAAMethod represents an AAA method. + properties: + groupName: + description: GroupName is the name of the server group when + Type is Group. + maxLength: 63 + type: string + type: + allOf: + - enum: + - Group + - Local + - None + - enum: + - Group + - Local + - None + description: Type is the type of AAA method. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: groupName is required when type is Group + rule: self.type != 'Group' || self.groupName != "" + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + deviceRef: + description: |- + DeviceName is the name of the Device this object belongs to. The Device object must exist in the same namespace. + Immutable. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + maxLength: 63 + minLength: 1 + type: string + required: + - name + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: DeviceRef is immutable + rule: self == oldSelf + providerConfigRef: + description: |- + ProviderConfigRef is a reference to a resource holding the provider-specific configuration of this AAA. + This reference is used to link the AAA to its provider-specific configuration. + properties: + apiVersion: + description: APIVersion is the api group version of the resource + being referenced. + maxLength: 253 + minLength: 1 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/)?([a-z0-9]([-a-z0-9]*[a-z0-9])?)$ + type: string + kind: + description: |- + Kind of the resource being referenced. + Kind must consist of alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name of the resource being referenced. + Name must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - apiVersion + - kind + - name + type: object + x-kubernetes-map-type: atomic + serverGroups: + description: |- + ServerGroups is the list of AAA server groups. + OpenConfig: /system/aaa/server-groups/server-group + items: + description: |- + AAAServerGroup represents a named group of AAA servers. + OpenConfig: /system/aaa/server-groups/server-group[name] + properties: + name: + description: Name is the name of the server group. + maxLength: 63 + minLength: 1 + type: string + servers: + description: |- + Servers is the list of servers in this group. + OpenConfig: /system/aaa/server-groups/server-group/servers/server + items: + description: |- + AAAServer represents a single AAA server within a group. + OpenConfig: /system/aaa/server-groups/server-group/servers/server[address] + properties: + address: + description: Address is the IP address or hostname of + the server. + maxLength: 253 + minLength: 1 + type: string + tacacs: + description: |- + TACACS contains TACACS+ specific server configuration. + Required when the parent server group type is TACACS. + OpenConfig augmentation: /system/aaa/server-groups/server-group/servers/server/tacacs + properties: + keySecretRef: + description: |- + KeySecretRef is a reference to a secret containing the shared key for this TACACS+ server. + The secret must contain a key specified in the SecretKeySelector. + properties: + key: + description: |- + Key is the of the entry in the secret resource's `data` or `stringData` + field to be used. + maxLength: 253 + minLength: 1 + type: string + name: + description: Name is unique within a namespace + to reference a secret resource. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace defines the space within which the secret name must be unique. + If omitted, the namespace of the object being reconciled will be used. + maxLength: 63 + minLength: 1 + type: string + required: + - key + - name + type: object + x-kubernetes-map-type: atomic + port: + default: 49 + description: |- + Port is the TCP port of the TACACS+ server. + Defaults to 49 if not specified. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - keySecretRef + type: object + timeout: + description: Timeout is the response timeout in seconds + for this server. + format: int32 + maximum: 60 + minimum: 1 + type: integer + required: + - address + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - address + x-kubernetes-list-type: map + sourceInterfaceName: + description: SourceInterfaceName is the source interface to + use for communication with the servers. + maxLength: 63 + type: string + type: + description: Type is the protocol type of this server group. + enum: + - TACACS + - RADIUS + type: string + vrfName: + description: VrfName is the VRF to use for communication with + the servers in this group. + maxLength: 63 + type: string + required: + - name + - servers + - type + type: object + x-kubernetes-validations: + - message: servers in a TACACS group must have tacacs config + rule: self.type != 'TACACS' || self.servers.all(s, has(s.tacacs)) + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + required: + - deviceRef + type: object + x-kubernetes-validations: + - message: at least one of serverGroups, authentication, authorization, + or accounting must be set + rule: has(self.serverGroups) || has(self.authentication) || has(self.authorization) + || has(self.accounting) + status: + description: |- + Status of the resource. This is set and updated automatically. + Read-only. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + conditions: + description: The conditions are a list of status objects that describe + the state of the AAA. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml new file mode 100644 index 00000000..4b1aba88 --- /dev/null +++ b/config/crd/bases/nx.cisco.networking.metal.ironcore.dev_aaaconfigs.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: aaaconfigs.nx.cisco.networking.metal.ironcore.dev +spec: + group: nx.cisco.networking.metal.ironcore.dev + names: + kind: AAAConfig + listKind: AAAConfigList + plural: aaaconfigs + shortNames: + - nxaaa + singular: aaaconfig + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: AAAConfig is the Schema for the aaaconfigs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + Specification of the desired state of the resource. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + configCommandsAuthorization: + description: |- + ConfigCommandsAuthorization defines NX-OS config-commands authorization methods. + Maps to: aaa authorization config-commands default + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: NXOSMethod represents a single AAA method in an + NX-OS context. + properties: + groupName: + description: GroupName is the server group name when Type + is Group. + maxLength: 63 + type: string + type: + description: Type is the method type. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + consoleAuthentication: + description: |- + ConsoleAuthentication defines NX-OS console-specific authentication methods. + Maps to: aaa authentication login console + properties: + methods: + description: Methods is the ordered list of methods. + items: + description: NXOSMethod represents a single AAA method in an + NX-OS context. + properties: + groupName: + description: GroupName is the server group name when Type + is Group. + maxLength: 63 + type: string + type: + description: Type is the method type. + enum: + - Group + - Local + - None + type: string + required: + - type + type: object + maxItems: 4 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + required: + - methods + type: object + keyEncryption: + allOf: + - enum: + - Type6 + - Type7 + - Clear + - enum: + - Type6 + - Type7 + - Clear + default: Type7 + description: KeyEncryption specifies the default encryption type for + TACACS+ keys. + type: string + loginErrorEnable: + description: |- + LoginErrorEnable enables login error messages (NX-OS specific). + Maps to: aaa authentication login error-enable + type: boolean + type: object + required: + - spec + type: object + served: true + storage: true diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index e0b4d529..aaaf7dab 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -24,7 +24,7 @@ resources: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus +- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [PROVISIONING] Expose the controller manager provisioning service. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5bf736dd..201eac6a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -42,6 +42,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas - accesscontrollists - banners - bgp @@ -76,6 +77,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas/finalizers - accesscontrollists/finalizers - banners/finalizers - bgp/finalizers @@ -104,6 +106,7 @@ rules: - apiGroups: - networking.metal.ironcore.dev resources: + - aaas/status - accesscontrollists/status - banners/status - bgp/status @@ -131,6 +134,17 @@ rules: - get - patch - update +- apiGroups: + - nx.cisco.networking.metal.ironcore.dev + resources: + - aaaconfigs + - interfaceconfigs + - managementaccessconfigs + - networkvirtualizationedgeconfigs + verbs: + - get + - list + - watch - apiGroups: - nx.cisco.networking.metal.ironcore.dev resources: diff --git a/config/samples/networking_v1alpha1_aaa.yaml b/config/samples/networking_v1alpha1_aaa.yaml new file mode 100644 index 00000000..94e2d6b0 --- /dev/null +++ b/config/samples/networking_v1alpha1_aaa.yaml @@ -0,0 +1,102 @@ +# Example AAA configuration with TACACS+ servers +# This configures: +# - feature tacacs+ +# - tacacs-server hosts with encrypted keys +# - AAA server group for TACACS+ +# - AAA authentication, authorization, and accounting +--- +apiVersion: v1 +kind: Secret +metadata: + name: tacacs-server-keys + namespace: default +type: Opaque +stringData: + # Replace with your actual TACACS+ server shared secret + server-key: "supersecretkey" +--- +# Cisco NX-OS specific AAA configuration +# This configures vendor-specific settings like key encryption type, +# login error messages, console authentication, and config-commands authorization. +apiVersion: nx.cisco.networking.metal.ironcore.dev/v1alpha1 +kind: AAAConfig +metadata: + name: aaa-tacacs-nxos + namespace: default +spec: + keyEncryption: Type7 + loginErrorEnable: true + # NX-OS: aaa authentication login console group GR_TACACS local + consoleAuthentication: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + # NX-OS: aaa authorization config-commands default group GR_TACACS local + configCommandsAuthorization: + methods: + - type: Group + groupName: GR_TACACS + - type: Local +--- +apiVersion: networking.metal.ironcore.dev/v1alpha1 +kind: AAA +metadata: + name: aaa-tacacs + namespace: default +spec: + # Reference to the Device this AAA configuration belongs to + deviceRef: + name: my-switch + + # Reference to the Cisco NX-OS specific AAA configuration + providerConfigRef: + group: nx.cisco.networking.metal.ironcore.dev + kind: AAAConfig + name: aaa-tacacs-nxos + + # TACACS+ server group with nested servers + # OpenConfig: /system/aaa/server-groups/server-group + serverGroups: + - name: GR_TACACS + type: TACACS + vrfName: management + sourceInterfaceName: mgmt0 + servers: + - address: "10.16.8.142" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key + - address: "10.16.8.32" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key + - address: "169.145.33.51" + tacacs: + keySecretRef: + name: tacacs-server-keys + key: server-key + + # AAA authentication method list + # OpenConfig: /system/aaa/authentication + authentication: + methods: + - type: Group + groupName: GR_TACACS + + # AAA authorization method list + # OpenConfig: /system/aaa/authorization + authorization: + methods: + - type: Group + groupName: GR_TACACS + - type: Local + + # AAA accounting method list + # OpenConfig: /system/aaa/accounting + accounting: + methods: + - type: Group + groupName: GR_TACACS diff --git a/hack/provider/main.go b/hack/provider/main.go index 26c82149..a29b277d 100644 --- a/hack/provider/main.go +++ b/hack/provider/main.go @@ -11,6 +11,7 @@ import ( "net/netip" "os" "os/signal" + "path/filepath" "reflect" "strings" "syscall" @@ -84,7 +85,8 @@ func (r *refStoreReader) List(ctx context.Context, list client.ObjectList, opts } func usage() { - fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", os.Args[0]) // #nosec G705 + base := filepath.Base(os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [flags] \n\n", base) fmt.Fprintf(os.Stderr, "A debug tool for testing provider implementations.\n\n") fmt.Fprintf(os.Stderr, "This tool allows you to directly test provider implementations by creating or\n") fmt.Fprintf(os.Stderr, "deleting resources on network devices.\n\n") @@ -93,7 +95,7 @@ func usage() { fmt.Fprintf(os.Stderr, "Flags:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nExample:\n") - fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", os.Args[0]) // #nosec G705 + fmt.Fprintf(os.Stderr, " %s -address=192.168.1.1:9339 -username=admin -password=secret -file=config/samples/v1alpha1_interface.yaml create\n", base) } func validateFlags() error { diff --git a/internal/controller/core/aaa_controller.go b/internal/controller/core/aaa_controller.go new file mode 100644 index 00000000..1370d88c --- /dev/null +++ b/internal/controller/core/aaa_controller.go @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + "context" + "errors" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/tools/record" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + "github.com/ironcore-dev/network-operator/internal/clientutil" + "github.com/ironcore-dev/network-operator/internal/conditions" + "github.com/ironcore-dev/network-operator/internal/deviceutil" + "github.com/ironcore-dev/network-operator/internal/provider" + "github.com/ironcore-dev/network-operator/internal/resourcelock" +) + +// AAAReconciler reconciles a AAA object +type AAAReconciler struct { + client.Client + Scheme *runtime.Scheme + + // WatchFilterValue is the label value used to filter events prior to reconciliation. + WatchFilterValue string + + // Recorder is used to record events for the controller. + // More info: https://book.kubebuilder.io/reference/raising-events + Recorder record.EventRecorder + + // Provider is the driver that will be used to create & delete the AAA configuration. + Provider provider.ProviderFunc + + // Locker is used to synchronize operations on resources targeting the same device. + Locker *resourcelock.ResourceLocker +} + +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=networking.metal.ironcore.dev,resources=aaas/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.20.2/pkg/reconcile +// +// For more details about the method shape, read up here: +// - https://ahmet.im/blog/controller-pitfalls/#reconcile-method-shape +func (r *AAAReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + log := ctrl.LoggerFrom(ctx) + log.Info("Reconciling resource") + + obj := new(v1alpha1.AAA) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + // If the custom resource is not found then it usually means that it was deleted or not created + // In this way, we will stop the reconciliation + log.Info("Resource not found. Ignoring since object must be deleted") + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + log.Error(err, "Failed to get resource") + return ctrl.Result{}, err + } + + prov, ok := r.Provider().(provider.AAAProvider) + if !ok { + if meta.SetStatusCondition(&obj.Status.Conditions, metav1.Condition{ + Type: v1alpha1.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: v1alpha1.NotImplementedReason, + Message: "Provider does not implement provider.AAAProvider", + }) { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + return ctrl.Result{}, nil + } + + device, err := deviceutil.GetDeviceByName(ctx, r, obj.Namespace, obj.Spec.DeviceRef.Name) + if err != nil { + return ctrl.Result{}, err + } + + if err := r.Locker.AcquireLock(ctx, device.Name, "aaa-controller"); err != nil { + if errors.Is(err, resourcelock.ErrLockAlreadyHeld) { + log.Info("Device is already locked, requeuing reconciliation") + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + log.Error(err, "Failed to acquire device lock") + return ctrl.Result{}, err + } + defer func() { + if err := r.Locker.ReleaseLock(ctx, device.Name, "aaa-controller"); err != nil { + log.Error(err, "Failed to release device lock") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + conn, err := deviceutil.GetDeviceConnection(ctx, r, device) + if err != nil { + return ctrl.Result{}, err + } + + var cfg *provider.ProviderConfig + if obj.Spec.ProviderConfigRef != nil { + cfg, err = provider.GetProviderConfig(ctx, r, obj.Namespace, obj.Spec.ProviderConfigRef) + if err != nil { + return ctrl.Result{}, err + } + } + + s := &aaaScope{ + Device: device, + AAA: obj, + Connection: conn, + ProviderConfig: cfg, + Provider: prov, + } + + if !obj.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + if err := r.finalize(ctx, s); err != nil { + log.Error(err, "Failed to finalize resource") + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to remove finalizer from resource") + return ctrl.Result{}, err + } + } + log.Info("Resource is being deleted, skipping reconciliation") + return ctrl.Result{}, nil + } + + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers + if !controllerutil.ContainsFinalizer(obj, v1alpha1.FinalizerName) { + controllerutil.AddFinalizer(obj, v1alpha1.FinalizerName) + if err := r.Update(ctx, obj); err != nil { + log.Error(err, "Failed to add finalizer to resource") + return ctrl.Result{}, err + } + log.Info("Added finalizer to resource") + return ctrl.Result{}, nil + } + + orig := obj.DeepCopy() + if conditions.InitializeConditions(obj, v1alpha1.ReadyCondition) { + log.Info("Initializing status conditions") + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + + // Always attempt to update the metadata/status after reconciliation + defer func() { + if !equality.Semantic.DeepEqual(orig.ObjectMeta, obj.ObjectMeta) { + if err := r.Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update resource metadata") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + return + } + + if !equality.Semantic.DeepEqual(orig.Status, obj.Status) { + if err := r.Status().Patch(ctx, obj, client.MergeFrom(orig)); err != nil { + log.Error(err, "Failed to update status") + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + } + }() + + if err := r.reconcile(ctx, s); err != nil { + log.Error(err, "Failed to reconcile resource") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *AAAReconciler) SetupWithManager(mgr ctrl.Manager) error { + labelSelector := metav1.LabelSelector{} + if r.WatchFilterValue != "" { + labelSelector.MatchLabels = map[string]string{v1alpha1.WatchLabel: r.WatchFilterValue} + } + + filter, err := predicate.LabelSelectorPredicate(labelSelector) + if err != nil { + return fmt.Errorf("failed to create label selector predicate: %w", err) + } + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.AAA{}). + Named("aaa"). + WithEventFilter(filter). + // Watches enqueues AAA for referenced Secret resources. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.secretToAAA), + builder.WithPredicates(predicate.GenerationChangedPredicate{}), + ). + Complete(r) +} + +// aaaScope holds the different objects that are read and used during the reconcile. +type aaaScope struct { + Device *v1alpha1.Device + AAA *v1alpha1.AAA + Connection *deviceutil.Connection + ProviderConfig *provider.ProviderConfig + Provider provider.AAAProvider +} + +func (r *AAAReconciler) reconcile(ctx context.Context, s *aaaScope) (reterr error) { + if s.AAA.Labels == nil { + s.AAA.Labels = make(map[string]string) + } + + s.AAA.Labels[v1alpha1.DeviceLabel] = s.Device.Name + + // Ensure the AAA is owned by the Device. + if !controllerutil.HasControllerReference(s.AAA) { + if err := controllerutil.SetOwnerReference(s.Device, s.AAA, r.Scheme, controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return err + } + } + + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + // Load server keys from secrets + c := clientutil.NewClient(r, s.AAA.Namespace) + tacacsKeys := make(map[string]string) + radiusKeys := make(map[string]string) + for _, group := range s.AAA.Spec.ServerGroups { + for _, server := range group.Servers { + if server.TACACS != nil { + key, err := c.Secret(ctx, &server.TACACS.KeySecretRef) + if err != nil { + return fmt.Errorf("failed to get key for server %s in group %s: %w", server.Address, group.Name, err) + } + tacacsKeys[server.Address] = string(key) + } + if server.RADIUS != nil { + key, err := c.Secret(ctx, &server.RADIUS.KeySecretRef) + if err != nil { + return fmt.Errorf("failed to get key for server %s in group %s: %w", server.Address, group.Name, err) + } + radiusKeys[server.Address] = string(key) + } + } + } + + // Ensure the AAA is realized on the provider. + err := s.Provider.EnsureAAA(ctx, &provider.EnsureAAARequest{ + AAA: s.AAA, + ProviderConfig: s.ProviderConfig, + TACACSServerKeys: tacacsKeys, + RADIUSServerKeys: radiusKeys, + }) + + cond := conditions.FromError(err) + // As this resource is configuration only, we use the Configured condition as top-level Ready condition. + cond.Type = v1alpha1.ReadyCondition + conditions.Set(s.AAA, cond) + + return err +} + +func (r *AAAReconciler) finalize(ctx context.Context, s *aaaScope) (reterr error) { + if err := s.Provider.Connect(ctx, s.Connection); err != nil { + return fmt.Errorf("failed to connect to provider: %w", err) + } + defer func() { + if err := s.Provider.Disconnect(ctx, s.Connection); err != nil { + reterr = kerrors.NewAggregate([]error{reterr, err}) + } + }() + + return s.Provider.DeleteAAA(ctx, &provider.DeleteAAARequest{ + AAA: s.AAA, + ProviderConfig: s.ProviderConfig, + }) +} + +// secretToAAA is a [handler.MapFunc] to be used to enqueue requests for reconciliation +// for an AAA to update when one of its referenced Secrets gets updated. +func (r *AAAReconciler) secretToAAA(ctx context.Context, obj client.Object) []ctrl.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + panic(fmt.Sprintf("Expected a Secret but got a %T", obj)) + } + + log := ctrl.LoggerFrom(ctx, "Secret", klog.KObj(secret)) + + aaas := new(v1alpha1.AAAList) + if err := r.List(ctx, aaas); err != nil { + log.Error(err, "Failed to list AAAs") + return nil + } + + requests := []ctrl.Request{} + for _, a := range aaas.Items { + found := false + for _, group := range a.Spec.ServerGroups { + for _, server := range group.Servers { + if (server.TACACS != nil && server.TACACS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) || + (server.RADIUS != nil && server.RADIUS.KeySecretRef.Name == secret.Name && a.Namespace == secret.Namespace) { + log.Info("Enqueuing AAA for reconciliation", "AAA", klog.KObj(&a)) + requests = append(requests, ctrl.Request{ + NamespacedName: client.ObjectKey{ + Name: a.Name, + Namespace: a.Namespace, + }, + }) + found = true + break + } + } + if found { + break // Only enqueue once per AAA + } + } + } + + return requests +} diff --git a/internal/deviceutil/deviceutil.go b/internal/deviceutil/deviceutil.go index abde29f1..a82a2e47 100644 --- a/internal/deviceutil/deviceutil.go +++ b/internal/deviceutil/deviceutil.go @@ -90,7 +90,7 @@ type Connection struct { // Username for basic authentication. Might be empty if the device does not require authentication. Username string // Password for basic authentication. Might be empty if the device does not require authentication. - Password string // #nosec G117 + Password string `json:"-"` // TLS configuration for the connection. TLS *tls.Config } @@ -184,7 +184,7 @@ func WithDefaultTimeout(timeout time.Duration) Option { type auth struct { Username string - Password string // #nosec G117 + Password string `json:"-"` SecureTransportCreds bool } diff --git a/internal/provider/cisco/nxos/aaa.go b/internal/provider/cisco/nxos/aaa.go new file mode 100644 index 00000000..2c899587 --- /dev/null +++ b/internal/provider/cisco/nxos/aaa.go @@ -0,0 +1,301 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: Apache-2.0 + +package nxos + +import ( + nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1" + "github.com/ironcore-dev/network-operator/api/core/v1alpha1" + gnmiext "github.com/ironcore-dev/network-operator/internal/provider/cisco/gnmiext/v2" +) + +var ( + _ gnmiext.Configurable = (*TACACSFeature)(nil) + _ gnmiext.Configurable = (*TacacsPlusProvider)(nil) + _ gnmiext.Configurable = (*TacacsPlusProviderGroup)(nil) + _ gnmiext.Configurable = (*RadiusProvider)(nil) + _ gnmiext.Configurable = (*RadiusProviderGroup)(nil) + _ gnmiext.Configurable = (*AAADefaultAuth)(nil) + _ gnmiext.Configurable = (*AAAConsoleAuth)(nil) + _ gnmiext.Configurable = (*AAADefaultAuthor)(nil) + _ gnmiext.Configurable = (*AAADefaultAcc)(nil) +) + +// TACACSFeature enables/disables the TACACS+ feature on NX-OS. +type TACACSFeature string + +func (*TACACSFeature) XPath() string { + return "System/fm-items/tacacsplus-items/adminSt" +} + +const ( + TACACSFeatureEnabled TACACSFeature = "enabled" + TACACSFeatureDisabled TACACSFeature = "disabled" +) + +// AAA configuration constants +const ( + AAARealmTacacs = "tacacs" + AAARealmRadius = "radius" + AAARealmLocal = "local" + AAARealmNone = "none" + AAAValueYes = "yes" + AAAValueNo = "no" +) + +// TacacsPlusProvider represents a TACACS+ server host configuration. +type TacacsPlusProvider struct { + Name string `json:"name"` + Port int32 `json:"port,omitempty"` + Key string `json:"key,omitempty"` + KeyEnc string `json:"keyEnc,omitempty"` + Timeout int32 `json:"timeout,omitempty"` + Retries int32 `json:"retries,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*TacacsPlusProvider) IsListItem() {} + +func (p *TacacsPlusProvider) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovider-items/TacacsPlusProvider-list[name=" + p.Name + "]" +} + +// TacacsPlusProviderGroup represents a TACACS+ server group configuration. +type TacacsPlusProviderGroup struct { + Name string `json:"name"` + Vrf string `json:"vrf,omitempty"` + SrcIf string `json:"srcIf,omitempty"` + Deadtime int32 `json:"deadtime,omitempty"` + ProviderRefItems TacacsPlusProviderGroupRefItems `json:"providerref-items,omitzero"` +} + +func (*TacacsPlusProviderGroup) IsListItem() {} + +func (g *TacacsPlusProviderGroup) XPath() string { + return "System/userext-items/tacacsext-items/tacacsplusprovidergroup-items/TacacsPlusProviderGroup-list[name=" + g.Name + "]" +} + +type TacacsPlusProviderGroupRefItems struct { + ProviderRefList gnmiext.List[string, *TacacsPlusProviderRef] `json:"ProviderRef-list,omitzero"` +} + +type TacacsPlusProviderRef struct { + Name string `json:"name"` +} + +func (r *TacacsPlusProviderRef) Key() string { return r.Name } + +// RadiusProvider represents a RADIUS server host configuration. +type RadiusProvider struct { + Name string `json:"name"` + AuthPort int32 `json:"authPort,omitempty"` + AcctPort int32 `json:"acctPort,omitempty"` + Key string `json:"key,omitempty"` + KeyEnc string `json:"keyEnc,omitempty"` + Timeout int32 `json:"timeout,omitempty"` + Retries int32 `json:"retries,omitempty"` +} + +func (*RadiusProvider) IsListItem() {} + +func (p *RadiusProvider) XPath() string { + return "System/userext-items/radiusext-items/radiusprovider-items/RadiusProvider-list[name=" + p.Name + "]" +} + +// RadiusProviderGroup represents a RADIUS server group configuration. +type RadiusProviderGroup struct { + Name string `json:"name"` + Vrf string `json:"vrf,omitempty"` + SrcIf string `json:"srcIf,omitempty"` + Deadtime int32 `json:"deadtime,omitempty"` + ProviderRefItems RadiusProviderGroupRefItems `json:"providerref-items,omitzero"` +} + +func (*RadiusProviderGroup) IsListItem() {} + +func (g *RadiusProviderGroup) XPath() string { + return "System/userext-items/radiusext-items/radiusprovidergroup-items/RadiusProviderGroup-list[name=" + g.Name + "]" +} + +type RadiusProviderGroupRefItems struct { + ProviderRefList gnmiext.List[string, *RadiusProviderRef] `json:"ProviderRef-list,omitzero"` +} + +type RadiusProviderRef struct { + Name string `json:"name"` +} + +func (r *RadiusProviderRef) Key() string { return r.Name } + +// AAADefaultAuth represents AAA default authentication configuration. +type AAADefaultAuth struct { + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + Fallback string `json:"fallback,omitempty"` + Local string `json:"local,omitempty"` + None string `json:"none,omitempty"` + ErrEn bool `json:"errEn,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*AAADefaultAuth) XPath() string { + return "System/userext-items/authrealm-items/defaultauth-items" +} + +// AAAConsoleAuth represents AAA console authentication configuration. +type AAAConsoleAuth struct { + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + Fallback string `json:"fallback,omitempty"` + Local string `json:"local,omitempty"` + None string `json:"none,omitempty"` + ErrEn bool `json:"errEn,omitempty"` + AuthProtocol string `json:"authProtocol,omitempty"` +} + +func (*AAAConsoleAuth) XPath() string { + return "System/userext-items/authrealm-items/consoleauth-items" +} + +// AAADefaultAuthor represents AAA default authorization configuration for config commands. +// Note: "name" and "realm" are read-only operational fields on NX-OS and must not be sent. +type AAADefaultAuthor struct { + CmdType string `json:"cmdType"` + ProviderGroup string `json:"providerGroup,omitempty"` + LocalRbac bool `json:"localRbac,omitempty"` + AuthorMethodNone bool `json:"authorMethodNone,omitempty"` +} + +func (*AAADefaultAuthor) IsListItem() {} + +func (a *AAADefaultAuthor) XPath() string { + return "System/userext-items/authrealm-items/defaultauthor-items/DefaultAuthor-list[cmdType=" + a.CmdType + "]" +} + +// AAADefaultAcc represents AAA default accounting configuration. +type AAADefaultAcc struct { + Name string `json:"name,omitempty"` + Realm string `json:"realm,omitempty"` + ProviderGroup string `json:"providerGroup,omitempty"` + LocalRbac bool `json:"localRbac,omitempty"` + AccMethodNone bool `json:"accMethodNone,omitempty"` +} + +func (*AAADefaultAcc) XPath() string { + return "System/userext-items/authrealm-items/defaultacc-items" +} + +// MapKeyEncryption maps the Cisco-specific key encryption type to NX-OS type. +func MapKeyEncryption(enc nxv1alpha1.TACACSKeyEncryption) string { + switch enc { + case nxv1alpha1.TACACSKeyEncryptionType6: + return "6" + case nxv1alpha1.TACACSKeyEncryptionType7: + return "7" + case nxv1alpha1.TACACSKeyEncryptionClear: + return "0" + default: + return "7" + } +} + +// MapRADIUSKeyEncryption maps the Cisco-specific RADIUS key encryption type to NX-OS type. +func MapRADIUSKeyEncryption(enc nxv1alpha1.RADIUSKeyEncryption) string { + switch enc { + case nxv1alpha1.RADIUSKeyEncryptionType6: + return "6" + case nxv1alpha1.RADIUSKeyEncryptionType7: + return "7" + case nxv1alpha1.RADIUSKeyEncryptionClear: + return "0" + default: + return "7" + } +} + +// groupTypeByName returns the AAAServerGroupType for the given group name, +// defaulting to TACACS if not found. +func groupTypeByName(name string, groups []v1alpha1.AAAServerGroup) v1alpha1.AAAServerGroupType { + for _, g := range groups { + if g.Name == name { + return g.Type + } + } + return v1alpha1.AAAServerGroupTypeTACACS +} + +// MapRealmFromGroup returns the NX-OS realm string for the given group name, +// resolving TACACS vs RADIUS from the server group list. +func MapRealmFromGroup(groupName string, groups []v1alpha1.AAAServerGroup) string { + switch groupTypeByName(groupName, groups) { + case v1alpha1.AAAServerGroupTypeRADIUS: + return AAARealmRadius + default: + return AAARealmTacacs + } +} + +// MapRealmFromMethodType maps the API method type to NX-OS realm. +func MapRealmFromMethodType(method v1alpha1.AAAMethodType) string { + switch method { + case v1alpha1.AAAMethodTypeGroup: + return AAARealmTacacs + case v1alpha1.AAAMethodTypeLocal: + return AAARealmLocal + case v1alpha1.AAAMethodTypeNone: + return AAARealmNone + default: + return AAARealmLocal + } +} + +// MapLocalFromMethodList checks if local is in the method list. +func MapLocalFromMethodList(methods []v1alpha1.AAAMethod) string { + for _, m := range methods { + if m.Type == v1alpha1.AAAMethodTypeLocal { + return AAAValueYes + } + } + return AAAValueNo +} + +// MapFallbackFromMethodList determines fallback setting from method list. +func MapFallbackFromMethodList(methods []v1alpha1.AAAMethod) string { + // If there's more than one method, enable fallback + if len(methods) > 1 { + return AAAValueYes + } + return AAAValueNo +} + +// MapNXOSRealm maps an NX-OS method type string to NX-OS realm. +func MapNXOSRealm(methodType string) string { + switch methodType { + case "Group": + return AAARealmTacacs + case "Local": + return AAARealmLocal + case "None": + return AAARealmNone + default: + return AAARealmLocal + } +} + +// MapNXOSLocal checks if local is in an NX-OS method list. +func MapNXOSLocal(methods []nxv1alpha1.NXOSMethod) string { + for _, m := range methods { + if m.Type == "Local" { + return AAAValueYes + } + } + return AAAValueNo +} + +// MapNXOSFallback determines fallback setting from an NX-OS method list. +func MapNXOSFallback(methods []nxv1alpha1.NXOSMethod) string { + if len(methods) > 1 { + return AAAValueYes + } + return AAAValueNo +} diff --git a/internal/provider/cisco/nxos/ascii.go b/internal/provider/cisco/nxos/ascii.go index 47197f95..564b3089 100644 --- a/internal/provider/cisco/nxos/ascii.go +++ b/internal/provider/cisco/nxos/ascii.go @@ -18,8 +18,7 @@ func (s ASCIIStr) String() string { if v == "0" { break } - if num, err := strconv.Atoi(v); err == nil { - // #nosec G115 + if num, err := strconv.Atoi(v); err == nil && num >= 0 && num <= 127 { runes = append(runes, rune(num)) } } diff --git a/internal/provider/cisco/nxos/provider.go b/internal/provider/cisco/nxos/provider.go index a51266ea..9f7807fa 100644 --- a/internal/provider/cisco/nxos/provider.go +++ b/internal/provider/cisco/nxos/provider.go @@ -57,6 +57,7 @@ var ( _ provider.VLANProvider = (*Provider)(nil) _ provider.VRFProvider = (*Provider)(nil) _ provider.NVEProvider = (*Provider)(nil) + _ provider.AAAProvider = (*Provider)(nil) _ provider.LLDPProvider = (*Provider)(nil) ) @@ -2791,6 +2792,260 @@ func separateFeatureActivation(conf []gnmiext.Configurable) (features, others [] return fa, conf[:n:n] } +func (p *Provider) EnsureAAA(ctx context.Context, req *provider.EnsureAAARequest) error { + var conf []gnmiext.Configurable + + // Read Cisco-specific config from ProviderConfig + var cfg nxv1alpha1.AAAConfig + if req.ProviderConfig != nil { + if err := req.ProviderConfig.Into(&cfg); err != nil { + return err + } + } + + // Process server groups + for _, group := range req.AAA.Spec.ServerGroups { + switch group.Type { + case v1alpha1.AAAServerGroupTypeTACACS: + // Enable TACACS+ feature + tacacsFeature := TACACSFeatureEnabled + conf = append(conf, &tacacsFeature) + + // Configure individual TACACS+ server hosts + for _, server := range group.Servers { + srv := &TacacsPlusProvider{ + Name: server.Address, + KeyEnc: MapKeyEncryption(cfg.Spec.KeyEncryption), + } + if server.TACACS != nil { + srv.Port = server.TACACS.Port + } + if key, ok := req.TACACSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = *server.Timeout + } + conf = append(conf, srv) + } + + // Configure the TACACS+ server group + grp := &TacacsPlusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, + } + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&TacacsPlusProviderRef{Name: server.Address}) + } + conf = append(conf, grp) + + case v1alpha1.AAAServerGroupTypeRADIUS: + // Configure individual RADIUS server hosts + for _, server := range group.Servers { + srv := &RadiusProvider{ + Name: server.Address, + KeyEnc: MapRADIUSKeyEncryption(cfg.Spec.RADIUSKeyEncryption), + } + if server.RADIUS != nil { + srv.AuthPort = server.RADIUS.AuthPort + srv.AcctPort = server.RADIUS.AcctPort + } + if key, ok := req.RADIUSServerKeys[server.Address]; ok { + srv.Key = key + } + if server.Timeout != nil { + srv.Timeout = *server.Timeout + } + conf = append(conf, srv) + } + + // Configure the RADIUS server group + grp := &RadiusProviderGroup{ + Name: group.Name, + Vrf: group.VrfName, + SrcIf: group.SourceInterfaceName, + } + for _, server := range group.Servers { + grp.ProviderRefItems.ProviderRefList.Set(&RadiusProviderRef{Name: server.Address}) + } + conf = append(conf, grp) + } + } + + // Configure AAA default authentication (from core API flat method list) + if req.AAA.Spec.Authentication != nil && len(req.AAA.Spec.Authentication.Methods) > 0 { + methods := req.AAA.Spec.Authentication.Methods + authen := &AAADefaultAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapFallbackFromMethodList(methods), + Local: MapLocalFromMethodList(methods), + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + authen.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + authen.ProviderGroup = methods[0].GroupName + } else { + authen.Realm = MapRealmFromMethodType(methods[0].Type) + } + conf = append(conf, authen) + } + + // Configure AAA console authentication (from Cisco AAAConfig) + if cfg.Spec.ConsoleAuthentication != nil && len(cfg.Spec.ConsoleAuthentication.Methods) > 0 { + methods := cfg.Spec.ConsoleAuthentication.Methods + consoleAuth := &AAAConsoleAuth{ + ErrEn: cfg.Spec.LoginErrorEnable, + Fallback: MapNXOSFallback(methods), + Local: MapNXOSLocal(methods), + } + if methods[0].Type == "Group" { + consoleAuth.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + consoleAuth.ProviderGroup = methods[0].GroupName + } else { + consoleAuth.Realm = MapNXOSRealm(methods[0].Type) + } + conf = append(conf, consoleAuth) + } + + // Configure AAA authorization (from core API flat method list) + if req.AAA.Spec.Authorization != nil && len(req.AAA.Spec.Authorization.Methods) > 0 { + methods := req.AAA.Spec.Authorization.Methods + author := &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + author.ProviderGroup = methods[0].GroupName + } + conf = append(conf, author) + } + + // Configure AAA config-commands authorization (from Cisco AAAConfig) + if cfg.Spec.ConfigCommandsAuthorization != nil && len(cfg.Spec.ConfigCommandsAuthorization.Methods) > 0 { + methods := cfg.Spec.ConfigCommandsAuthorization.Methods + author := &AAADefaultAuthor{ + CmdType: "config", + LocalRbac: MapNXOSLocal(methods) == AAAValueYes, + } + if methods[0].Type == "Group" { + author.ProviderGroup = methods[0].GroupName + } + conf = append(conf, author) + } + + // Configure AAA accounting (from core API flat method list) + if req.AAA.Spec.Accounting != nil && len(req.AAA.Spec.Accounting.Methods) > 0 { + methods := req.AAA.Spec.Accounting.Methods + acct := &AAADefaultAcc{ + Name: "Accounting", + LocalRbac: MapLocalFromMethodList(methods) == AAAValueYes, + } + if methods[0].Type == v1alpha1.AAAMethodTypeGroup { + acct.Realm = MapRealmFromGroup(methods[0].GroupName, req.AAA.Spec.ServerGroups) + acct.ProviderGroup = methods[0].GroupName + } else { + acct.Realm = MapRealmFromMethodType(methods[0].Type) + } + conf = append(conf, acct) + } + + return p.Update(ctx, conf...) +} + +func (p *Provider) DeleteAAA(ctx context.Context, req *provider.DeleteAAARequest) error { + var conf []gnmiext.Configurable + + // Read Cisco-specific config from ProviderConfig + var cfg nxv1alpha1.AAAConfig + if req.ProviderConfig != nil { + if err := req.ProviderConfig.Into(&cfg); err != nil { + return err + } + } + + // Reset AAA accounting to local + if req.AAA.Spec.Accounting != nil { + conf = append(conf, &AAADefaultAcc{ + Name: "Accounting", + Realm: AAARealmLocal, + LocalRbac: true, + }) + } + + // Reset AAA authorization to local + if req.AAA.Spec.Authorization != nil || cfg.Spec.ConfigCommandsAuthorization != nil { + conf = append(conf, &AAADefaultAuthor{ + CmdType: "config", + ProviderGroup: "", + LocalRbac: true, + }) + } + + // Reset AAA authentication to local + if req.AAA.Spec.Authentication != nil { + conf = append(conf, &AAADefaultAuth{ + Realm: AAARealmLocal, + Local: AAAValueYes, + Fallback: AAAValueYes, + ErrEn: false, + }) + } + + // Reset console authentication to local + if cfg.Spec.ConsoleAuthentication != nil { + conf = append(conf, &AAAConsoleAuth{ + Realm: AAARealmLocal, + Local: AAAValueYes, + Fallback: AAAValueYes, + ErrEn: false, + }) + } + + // Delete server groups and hosts + hasTACACS := false + for _, group := range req.AAA.Spec.ServerGroups { + switch group.Type { + case v1alpha1.AAAServerGroupTypeTACACS: + hasTACACS = true + + grp := &TacacsPlusProviderGroup{Name: group.Name} + if err := p.client.Delete(ctx, grp); err != nil { + return err + } + for _, server := range group.Servers { + srv := &TacacsPlusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { + return err + } + } + + case v1alpha1.AAAServerGroupTypeRADIUS: + grp := &RadiusProviderGroup{Name: group.Name} + if err := p.client.Delete(ctx, grp); err != nil { + return err + } + for _, server := range group.Servers { + srv := &RadiusProvider{Name: server.Address} + if err := p.client.Delete(ctx, srv); err != nil { + return err + } + } + } + } + + // Disable TACACS+ feature + if hasTACACS { + tacacsFeature := TACACSFeatureDisabled + conf = append(conf, &tacacsFeature) + } + + if len(conf) > 0 { + return p.Update(ctx, conf...) + } + + return nil +} + func init() { provider.Register("cisco-nxos-gnmi", NewProvider) } diff --git a/internal/provider/cisco/nxos/user.go b/internal/provider/cisco/nxos/user.go index 1bb4a99e..16e1ddc9 100644 --- a/internal/provider/cisco/nxos/user.go +++ b/internal/provider/cisco/nxos/user.go @@ -25,7 +25,7 @@ type User struct { AllowExpired string `json:"allowExpired"` Expiration string `json:"expiration"` Name string `json:"name"` - Pwd string `json:"pwd,omitempty"` // #nosec G117 + Pwd string `json:"pwd,omitempty"` PwdHash PwdHashType `json:"passwordHash,omitempty"` PwdEncryptType PwdEncryptType `json:"pwdEncryptType,omitempty"` SshauthItems struct { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index eb0a5191..2eedf0f2 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -162,7 +162,7 @@ type UserProvider interface { type EnsureUserRequest struct { Username string - Password string // #nosec G117 + Password string `json:"-"` SSHKey string Roles []string ProviderConfig *ProviderConfig @@ -564,6 +564,32 @@ type NVEProvider interface { GetNVEStatus(context.Context, *NVERequest) (NVEStatus, error) } +// AAAProvider is the interface for the realization of the AAA objects over different providers. +type AAAProvider interface { + Provider + + // EnsureAAA call is responsible for AAA realization on the provider. + EnsureAAA(context.Context, *EnsureAAARequest) error + // DeleteAAA call is responsible for AAA deletion on the provider. + DeleteAAA(context.Context, *DeleteAAARequest) error +} + +type EnsureAAARequest struct { + AAA *v1alpha1.AAA + ProviderConfig *ProviderConfig + // TACACSServerKeys contains the decrypted keys for each TACACS+ server, + // keyed by server address. + TACACSServerKeys map[string]string + // RADIUSServerKeys contains the decrypted shared secrets for each RADIUS server, + // keyed by server address. + RADIUSServerKeys map[string]string +} + +type DeleteAAARequest struct { + AAA *v1alpha1.AAA + ProviderConfig *ProviderConfig +} + type NVERequest struct { NVE *v1alpha1.NetworkVirtualizationEdge SourceInterface *v1alpha1.Interface diff --git a/test/e2e/util.go b/test/e2e/util.go index 3eabc631..474e6d74 100644 --- a/test/e2e/util.go +++ b/test/e2e/util.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" //nolint:revive @@ -37,7 +38,6 @@ func Run(cmd *exec.Cmd) (string, error) { } command := strings.Join(cmd.Args, " ") - // #nosec G705 _, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command) output, err := cmd.CombinedOutput() @@ -55,16 +55,15 @@ func Apply(resource string) error { if err != nil { return fmt.Errorf("failed to create temp file: %w", err) } - // #nosec G703 - defer func() { _ = os.Remove(file.Name()) }() + tmpPath := filepath.Clean(file.Name()) + defer func() { _ = os.Remove(tmpPath) }() if _, err = file.Write([]byte(resource)); err != nil { return fmt.Errorf("failed to write to temp file: %w", err) } if err = file.Close(); err != nil { return fmt.Errorf("failed to close temp file: %w", err) } - // #nosec G204 G702 - cmd := exec.Command("kubectl", "apply", "-f", file.Name()) + cmd := exec.Command("kubectl", "apply", "-f", tmpPath) if _, err = Run(cmd); err != nil { return fmt.Errorf("failed to apply resource: %w", err) } @@ -207,18 +206,17 @@ func LoadImageToKindClusterWithName(name string) error { return fmt.Errorf("failed to create temp file: %w", err) } _ = file.Close() - // #nosec G703 - defer func() { _ = os.Remove(file.Name()) }() + imgPath := filepath.Clean(file.Name()) + defer func() { _ = os.Remove(imgPath) }() // https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-save // https://docs.podman.io/en/v5.3.0/markdown/podman-save.1.html - // #nosec G702 - cmd := exec.Command(prov, "save", name, "--output", file.Name()) + cmd := exec.Command(prov, "save", name, "--output", imgPath) if _, err = Run(cmd); err != nil { return fmt.Errorf("failed to save image: %w", err) } - cmd = exec.Command("kind", "load", "image-archive", file.Name(), "--name", cluster) //nolint:gosec + cmd = exec.Command("kind", "load", "image-archive", imgPath, "--name", cluster) _, err = Run(cmd) return err } diff --git a/test/lab/main_test.go b/test/lab/main_test.go index 6f7e4780..b56e5340 100644 --- a/test/lab/main_test.go +++ b/test/lab/main_test.go @@ -158,7 +158,7 @@ func Apply() script.Cmd { var Endpoint = struct { Addr string User string - Pass string // #nosec G117 + Pass string `json:"-"` }{} // ReadEnv reads required environment variables and populates the global Endpoint struct.