Skip to content

Commit e673d0c

Browse files
authored
Secure SMB implementation for Ontap-Nas Create
1 parent 45f3cbc commit e673d0c

31 files changed

+1423
-74
lines changed

frontend/csi/controller_helpers/kubernetes/config.go

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2022 NetApp, Inc. All Rights Reserved.
1+
// Copyright 2025 NetApp, Inc. All Rights Reserved.
22

33
package kubernetes
44

@@ -24,6 +24,13 @@ const (
2424
CacheBackoffMultiplier = 1.414
2525
CacheBackoffMaxInterval = 5 * time.Second
2626

27+
NASTypeSMB = "smb"
28+
29+
SMBShareFullControlPermission = "full_control" // AD user Full Control permission
30+
SMBShareReadPermission = "read" // AD user Read Only permission
31+
SMBShareNoPermission = "no_access" // AD user No Access permission
32+
SMBShareChangePermission = "change" // AD user Change permission
33+
2734
// Kubernetes-defined storage class parameters
2835

2936
K8sFsType = "fsType"
@@ -37,31 +44,37 @@ const (
3744
AnnStorageProvisioner = "volume.beta.kubernetes.io/storage-provisioner"
3845

3946
// Orchestrator-defined annotations
40-
annPrefix = config.OrchestratorName + ".netapp.io"
41-
AnnProtocol = annPrefix + "/protocol"
42-
AnnSnapshotPolicy = annPrefix + "/snapshotPolicy"
43-
AnnSnapshotReserve = annPrefix + "/snapshotReserve"
44-
AnnSnapshotDir = annPrefix + "/snapshotDirectory"
45-
AnnUnixPermissions = annPrefix + "/unixPermissions"
46-
AnnExportPolicy = annPrefix + "/exportPolicy"
47-
AnnBlockSize = annPrefix + "/blockSize"
48-
AnnFileSystem = annPrefix + "/fileSystem"
49-
AnnCloneFromPVC = annPrefix + "/cloneFromPVC"
50-
AnnCloneFromSnapshot = annPrefix + "/cloneFromSnapshot"
51-
AnnSplitOnClone = annPrefix + "/splitOnClone"
52-
AnnNotManaged = annPrefix + "/notManaged"
53-
AnnImportOriginalName = annPrefix + "/importOriginalName"
54-
AnnImportBackendUUID = annPrefix + "/importBackendUUID"
55-
AnnInternalSnapshotName = annPrefix + "/internalSnapshotName"
56-
AnnMirrorRelationship = annPrefix + "/mirrorRelationship"
57-
AnnVolumeShareFromPVC = annPrefix + "/shareFromPVC"
58-
AnnVolumeCloneToNS = annPrefix + "/cloneToNamespace"
59-
AnnVolumeCloneFromNS = annPrefix + "/cloneFromNamespace"
60-
AnnVolumeShareToNS = annPrefix + "/shareToNamespace"
61-
AnnReadOnlyClone = annPrefix + "/readOnlyClone"
62-
AnnLUKSEncryption = annPrefix + "/luksEncryption" // import only
63-
AnnSkipRecoveryQueue = annPrefix + "/skipRecoveryQueue"
64-
AnnSelector = annPrefix + "/selector"
47+
prefix = config.OrchestratorName + ".netapp.io"
48+
AnnProtocol = prefix + "/protocol"
49+
AnnSnapshotPolicy = prefix + "/snapshotPolicy"
50+
AnnSnapshotReserve = prefix + "/snapshotReserve"
51+
AnnSnapshotDir = prefix + "/snapshotDirectory"
52+
AnnUnixPermissions = prefix + "/unixPermissions"
53+
AnnExportPolicy = prefix + "/exportPolicy"
54+
AnnBlockSize = prefix + "/blockSize"
55+
AnnFileSystem = prefix + "/fileSystem"
56+
AnnCloneFromPVC = prefix + "/cloneFromPVC"
57+
AnnCloneFromSnapshot = prefix + "/cloneFromSnapshot"
58+
AnnSplitOnClone = prefix + "/splitOnClone"
59+
AnnNotManaged = prefix + "/notManaged"
60+
AnnImportOriginalName = prefix + "/importOriginalName"
61+
AnnImportBackendUUID = prefix + "/importBackendUUID"
62+
AnnInternalSnapshotName = prefix + "/internalSnapshotName"
63+
AnnMirrorRelationship = prefix + "/mirrorRelationship"
64+
AnnVolumeShareFromPVC = prefix + "/shareFromPVC"
65+
AnnVolumeCloneToNS = prefix + "/cloneToNamespace"
66+
AnnVolumeCloneFromNS = prefix + "/cloneFromNamespace"
67+
AnnVolumeShareToNS = prefix + "/shareToNamespace"
68+
AnnReadOnlyClone = prefix + "/readOnlyClone"
69+
AnnLUKSEncryption = prefix + "/luksEncryption" // import only
70+
AnnSkipRecoveryQueue = prefix + "/skipRecoveryQueue"
71+
AnnSelector = prefix + "/selector"
72+
AnnSMBShareAdUserPermission = prefix + "/smbShareAdUserPermission"
73+
AnnSMBShareAdUser = prefix + "/smbShareAdUser"
74+
AnnSMBShareAccessControl = prefix + "/smbShareAccessControl"
75+
76+
// Orchestrator-defined storage class parameters
77+
SCParameterNASType = prefix + "/nasType"
6578
)
6679

6780
var features = map[controllerhelpers.Feature]*versionutils.Version{

frontend/csi/controller_helpers/kubernetes/helper.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
vsv1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
12+
"gopkg.in/yaml.v2"
1213
v1 "k8s.io/api/core/v1"
1314
k8sstoragev1 "k8s.io/api/storage/v1"
1415
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -40,6 +41,7 @@ import (
4041
func (h *helper) GetVolumeConfig(
4142
ctx context.Context, name string, _ int64, _ map[string]string, _ config.Protocol, _ []config.AccessMode,
4243
_ config.VolumeMode, fsType string, requisiteTopology, preferredTopology, _ []map[string]string,
44+
secrets map[string]string,
4345
) (*storage.VolumeConfig, error) {
4446
// Kubernetes CSI passes us the name of what will become a new PV
4547
pvName := name
@@ -91,8 +93,20 @@ func (h *helper) GetVolumeConfig(
9193

9294
// Create the volume config
9395
annotations := processPVCAnnotations(pvc, fsType)
96+
97+
// Process annotations from the storage class
98+
scAnnotations := processSCAnnotations(sc)
99+
94100
volumeConfig := getVolumeConfig(ctx, pvc, pvName, pvcSize, annotations, sc, requisiteTopology, preferredTopology)
95101

102+
// Update the volume config with the Access Control only if the storage class nasType parameter is SMB
103+
if sc.Parameters[SCParameterNASType] == NASTypeSMB {
104+
err = h.updateVolumeConfigWithSecureSMBAccessControl(ctx, volumeConfig, sc, annotations, scAnnotations, secrets)
105+
if err != nil {
106+
return nil, err
107+
}
108+
}
109+
96110
// Get clone annotations
97111
sourceSnapshotName := getAnnotation(annotations, AnnCloneFromSnapshot)
98112
sourcePVCName := getAnnotation(annotations, AnnCloneFromPVC)
@@ -679,6 +693,52 @@ func (h *helper) getSnapshotInternalNameFromAnnotation(
679693
return internalName, nil
680694
}
681695

696+
// updateVolumeConfigWithSecureSMBAccessControl update the volume config with the secure SMB access control from the
697+
// PVC annotations and the storage class annotations
698+
func (h *helper) updateVolumeConfigWithSecureSMBAccessControl(ctx context.Context, volumeConfig *storage.VolumeConfig,
699+
sc *k8sstoragev1.StorageClass, pvcAnnotations, scAnnotations, secret map[string]string,
700+
) error {
701+
if sc.Parameters[CSIParameterNodeStageSecretName] == "" || sc.Parameters[CSIParameterNodeStageSecretNamespace] == "" {
702+
return fmt.Errorf("missing required parameters %s and %s for secure SMB access control",
703+
CSIParameterNodeStageSecretName, CSIParameterNodeStageSecretNamespace)
704+
}
705+
706+
// Get the smb share Access Control from PVC annotations
707+
smbSharePVCAccessControlAnn := getAnnotation(pvcAnnotations, AnnSMBShareAccessControl)
708+
smbShareACL, err := getSMBShareAccessControlFromPVCAnnotation(smbSharePVCAccessControlAnn)
709+
if err != nil {
710+
return fmt.Errorf("failed to parse smb share access control from annotation: %v", err)
711+
}
712+
713+
adUserFromSC := getAnnotation(scAnnotations, AnnSMBShareAdUser)
714+
adUserPermissionFromSC := getAnnotation(scAnnotations, AnnSMBShareAdUserPermission)
715+
716+
// Set SecureSMBEnabled to true if adUserFromSC is present. This ensures Secure SMB is enabled only when required.
717+
if adUserFromSC != "" {
718+
volumeConfig.SecureSMBEnabled = true
719+
}
720+
721+
// Check if the adUserPermissionAnn is set, if not set it to the full control
722+
if adUserPermissionFromSC == "" {
723+
adUserPermissionFromSC = SMBShareFullControlPermission
724+
}
725+
726+
// Check if the adUserPermissionAnn is valid
727+
if !isValidAccessControlPermission(adUserPermissionFromSC) {
728+
return fmt.Errorf("invalid adUserPermission: %s. Valid adUserPermissions are %s, %s, %s, %s",
729+
adUserPermissionFromSC, SMBShareFullControlPermission, SMBShareReadPermission, SMBShareChangePermission, SMBShareNoPermission)
730+
}
731+
732+
// Check if adUser already exists in the smbShareACL, if not add it
733+
if _, exists := smbShareACL[adUserFromSC]; !exists {
734+
smbShareACL[adUserFromSC] = adUserPermissionFromSC
735+
}
736+
737+
volumeConfig.SMBShareACL = smbShareACL
738+
739+
return nil
740+
}
741+
682742
// RecordVolumeEvent accepts the name of a CSI volume (i.e. a PV name), finds the associated
683743
// PVC, and posts and event message on the PVC object with the K8S API server.
684744
func (h *helper) RecordVolumeEvent(ctx context.Context, name, eventType, reason, message string) {
@@ -743,6 +803,7 @@ func getVolumeConfig(
743803
requisiteTopology, preferredTopology []map[string]string,
744804
) *storage.VolumeConfig {
745805
var accessModes []config.AccessMode
806+
smbShareACL := make(map[string]string)
746807

747808
for _, pvcAccessMode := range pvc.Spec.AccessModes {
748809
accessModes = append(accessModes, config.AccessMode(pvcAccessMode))
@@ -820,6 +881,7 @@ func getVolumeConfig(
820881
Namespace: pvc.Namespace,
821882
RequestName: pvc.Name,
822883
SkipRecoveryQueue: getAnnotation(annotations, AnnSkipRecoveryQueue),
884+
SMBShareACL: smbShareACL,
823885
}
824886
}
825887

@@ -858,3 +920,68 @@ func processPVCAnnotations(pvc *v1.PersistentVolumeClaim, fsType string) map[str
858920

859921
return annotations
860922
}
923+
924+
// processSCAnnotations updates the annotations map with SC annotations.
925+
func processSCAnnotations(sc *k8sstoragev1.StorageClass) map[string]string {
926+
annotations := sc.Annotations
927+
if sc.Annotations == nil {
928+
annotations = make(map[string]string)
929+
}
930+
931+
return annotations
932+
}
933+
934+
// getSMBShareAccessControlFromPVCAnnotation parses the smbShareAccessControl annotation and updates the smbShareACL map
935+
func getSMBShareAccessControlFromPVCAnnotation(smbShareAccessControlAnn string) (map[string]string, error) {
936+
// Structure to hold the parsed smbShareAccessControlAnnotation
937+
parsedData := make(map[string][]string)
938+
939+
// Parse the smbShareAccessControl annotation
940+
if err := yaml.Unmarshal([]byte(smbShareAccessControlAnn), &parsedData); err != nil {
941+
return nil, fmt.Errorf("failed to parse smbShareAccessControl annotation: %v", err)
942+
}
943+
944+
// Convert the parsed data into the smbShareACL map
945+
smbShareACL := make(map[string]string)
946+
947+
// Define a priority map for access control permissions,
948+
// to determine the permission for user who is associated with multiple permissions
949+
permissionPriority := map[string]int{
950+
SMBShareFullControlPermission: 4, // Highest priority
951+
SMBShareChangePermission: 3,
952+
SMBShareReadPermission: 2,
953+
SMBShareNoPermission: 1, // Lowest priority
954+
}
955+
956+
for accessControlPermission, users := range parsedData {
957+
if !isValidAccessControlPermission(accessControlPermission) {
958+
return nil, fmt.Errorf("invalid access control permission %s in smbShareAccessControl annotation, "+
959+
"valid access control permissions are %s, %s,%s,%s ", accessControlPermission, SMBShareFullControlPermission,
960+
SMBShareReadPermission, SMBShareChangePermission, SMBShareNoPermission)
961+
} else {
962+
for _, user := range users {
963+
// Check if the user already exists in the smbShareACL map
964+
if existingPermission, exists := smbShareACL[user]; exists {
965+
// Compare the priority of the existing permission with the new one
966+
if permissionPriority[accessControlPermission] > permissionPriority[existingPermission] {
967+
smbShareACL[user] = accessControlPermission
968+
}
969+
} else {
970+
// If the user doesn't exist, add it to the map
971+
smbShareACL[user] = accessControlPermission
972+
}
973+
}
974+
}
975+
}
976+
return smbShareACL, nil
977+
}
978+
979+
// isValidAccessControlPermission checks if the provided permission is valid
980+
func isValidAccessControlPermission(permission string) bool {
981+
switch permission {
982+
case SMBShareFullControlPermission, SMBShareReadPermission, SMBShareChangePermission, SMBShareNoPermission:
983+
return true
984+
default:
985+
return false
986+
}
987+
}

0 commit comments

Comments
 (0)