diff --git a/pkg/api/serialization_test.go b/pkg/api/serialization_test.go index f5d0f7701..6a22c2eec 100644 --- a/pkg/api/serialization_test.go +++ b/pkg/api/serialization_test.go @@ -467,6 +467,8 @@ func originFuzzer(t *testing.T, seed int64) *randfill.Filler { scc.SupplementalGroups.Type = supGroupTypes[c.Rand.Intn(len(supGroupTypes))] fsGroupTypes := []securityapi.FSGroupStrategyType{securityapi.FSGroupStrategyMustRunAs, securityapi.FSGroupStrategyRunAsAny} scc.FSGroup.Type = fsGroupTypes[c.Rand.Intn(len(fsGroupTypes))] + runAsGroupTypes := []securityapi.RunAsGroupStrategyType{securityapi.RunAsGroupStrategyMustRunAs, securityapi.RunAsGroupStrategyMustRunAsRange, securityapi.RunAsGroupStrategyRunAsAny} + scc.RunAsGroup.Type = runAsGroupTypes[c.Rand.Intn(len(runAsGroupTypes))] // avoid the defaulting logic for this field by making it never nil allowPrivilegeEscalation := c.Bool() scc.AllowPrivilegeEscalation = &allowPrivilegeEscalation diff --git a/pkg/security/apis/security/types.go b/pkg/security/apis/security/types.go index 503484554..16a7c6918 100644 --- a/pkg/security/apis/security/types.go +++ b/pkg/security/apis/security/types.go @@ -75,6 +75,9 @@ type SecurityContextConstraints struct { SupplementalGroups SupplementalGroupsStrategyOptions // FSGroup is the strategy that will dictate what fs group is used by the SecurityContext. FSGroup FSGroupStrategyOptions + // RunAsGroup is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + // +optional + RunAsGroup RunAsGroupStrategyOptions // ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file // system. If the container specifically requests to run with a non-read only root file system // the SCC should deny the pod. @@ -177,6 +180,30 @@ type RunAsUserStrategyOptions struct { UIDRangeMax *int64 } +// RunAsGroupStrategyOptions defines the strategy type and any options used to create the strategy. +type RunAsGroupStrategyOptions struct { + // Type is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + Type RunAsGroupStrategyType + // GID is the group id that containers must run as. Required for the MustRunAs strategy if not using + // namespace/service account allocated gids. + GID *int64 + // GIDRangeMin defines the min value for a strategy that allocates by range. + GIDRangeMin *int64 + // GIDRangeMax defines the max value for a strategy that allocates by range. + GIDRangeMax *int64 + // Ranges are the allowed ranges of gids. If you would like to force a single + // gid then supply a single range with the same start and end. + Ranges []RunAsGroupIDRange +} + +// RunAsGroupIDRange provides a min/max of an allowed range of group IDs for RunAsGroup strategy. +type RunAsGroupIDRange struct { + // Min is the start of the range, inclusive. + Min *int64 + // Max is the end of the range, inclusive. + Max *int64 +} + // FSGroupStrategyOptions defines the strategy type and options used to create the strategy. type FSGroupStrategyOptions struct { // Type is the strategy that will dictate what FSGroup is used in the SecurityContext. @@ -220,6 +247,10 @@ type SupplementalGroupsStrategyType string // SecurityContext type FSGroupStrategyType string +// RunAsGroupStrategyType denotes strategy types for generating RunAsGroup values for a +// SecurityContext +type RunAsGroupStrategyType string + const ( // container must have SELinux labels of X applied. SELinuxStrategyMustRunAs SELinuxContextStrategyType = "MustRunAs" @@ -235,6 +266,13 @@ const ( // container may make requests for any uid. RunAsUserStrategyRunAsAny RunAsUserStrategyType = "RunAsAny" + // container must run as a particular gid. + RunAsGroupStrategyMustRunAs RunAsGroupStrategyType = "MustRunAs" + // container must run with a gid in a range. + RunAsGroupStrategyMustRunAsRange RunAsGroupStrategyType = "MustRunAsRange" + // container may make requests for any gid. + RunAsGroupStrategyRunAsAny RunAsGroupStrategyType = "RunAsAny" + // container must have FSGroup of X applied. FSGroupStrategyMustRunAs FSGroupStrategyType = "MustRunAs" // container may make requests for any FSGroup labels. diff --git a/pkg/security/apis/security/v1/conversion.go b/pkg/security/apis/security/v1/conversion.go index 0849aca91..7137e5ed8 100644 --- a/pkg/security/apis/security/v1/conversion.go +++ b/pkg/security/apis/security/v1/conversion.go @@ -11,6 +11,14 @@ func Convert_v1_SecurityContextConstraints_To_security_SecurityContextConstraint return autoConvert_v1_SecurityContextConstraints_To_security_SecurityContextConstraints(in, out, s) } +func Convert_v1_RunAsGroupStrategyOptions_To_security_RunAsGroupStrategyOptions(in *v1.RunAsGroupStrategyOptions, out *securityapi.RunAsGroupStrategyOptions, s conversion.Scope) error { + return autoConvert_v1_RunAsGroupStrategyOptions_To_security_RunAsGroupStrategyOptions(in, out, s) +} + +func Convert_security_RunAsGroupStrategyOptions_To_v1_RunAsGroupStrategyOptions(in *securityapi.RunAsGroupStrategyOptions, out *v1.RunAsGroupStrategyOptions, s conversion.Scope) error { + return autoConvert_security_RunAsGroupStrategyOptions_To_v1_RunAsGroupStrategyOptions(in, out, s) +} + func Convert_security_SecurityContextConstraints_To_v1_SecurityContextConstraints(in *securityapi.SecurityContextConstraints, out *v1.SecurityContextConstraints, s conversion.Scope) error { if err := autoConvert_security_SecurityContextConstraints_To_v1_SecurityContextConstraints(in, out, s); err != nil { return err @@ -27,3 +35,21 @@ func Convert_security_SecurityContextConstraints_To_v1_SecurityContextConstraint } return nil } + +// Convert_v1_IDRange_To_security_RunAsGroupIDRange converts v1.IDRange to internal RunAsGroupIDRange +func Convert_v1_IDRange_To_security_RunAsGroupIDRange(in *v1.IDRange, out *securityapi.RunAsGroupIDRange, s conversion.Scope) error { + out.Min = &in.Min + out.Max = &in.Max + return nil +} + +// Convert_security_RunAsGroupIDRange_To_v1_IDRange converts internal RunAsGroupIDRange to v1.IDRange +func Convert_security_RunAsGroupIDRange_To_v1_IDRange(in *securityapi.RunAsGroupIDRange, out *v1.IDRange, s conversion.Scope) error { + if in.Min != nil { + out.Min = *in.Min + } + if in.Max != nil { + out.Max = *in.Max + } + return nil +} diff --git a/pkg/security/apis/security/v1/defaults.go b/pkg/security/apis/security/v1/defaults.go index e9fda4d98..eda7031ad 100644 --- a/pkg/security/apis/security/v1/defaults.go +++ b/pkg/security/apis/security/v1/defaults.go @@ -9,7 +9,21 @@ import ( func AddDefaultingFuncs(scheme *runtime.Scheme) error { RegisterDefaults(scheme) scheme.AddTypeDefaultingFunc(&v1.SecurityContextConstraints{}, func(obj interface{}) { - sccdefaults.SetDefaults_SCC(obj.(*v1.SecurityContextConstraints)) + scc := obj.(*v1.SecurityContextConstraints) + sccdefaults.SetDefaults_SCC(scc) + + // Default RunAsGroup to MustRunAs with ranges if not set + if len(scc.RunAsGroup.Type) == 0 { + min := int64(1000) + max := int64(65534) + scc.RunAsGroup.Type = v1.RunAsGroupStrategyMustRunAs + scc.RunAsGroup.Ranges = []v1.RunAsGroupIDRange{ + { + Min: &min, + Max: &max, + }, + } + } }) return nil diff --git a/pkg/security/apis/security/v1/zz_generated.conversion.go b/pkg/security/apis/security/v1/zz_generated.conversion.go index a05175874..5ada8c56b 100644 --- a/pkg/security/apis/security/v1/zz_generated.conversion.go +++ b/pkg/security/apis/security/v1/zz_generated.conversion.go @@ -154,6 +154,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*securityv1.RunAsGroupIDRange)(nil), (*security.RunAsGroupIDRange)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RunAsGroupIDRange_To_security_RunAsGroupIDRange(a.(*securityv1.RunAsGroupIDRange), b.(*security.RunAsGroupIDRange), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*security.RunAsGroupIDRange)(nil), (*securityv1.RunAsGroupIDRange)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_security_RunAsGroupIDRange_To_v1_RunAsGroupIDRange(a.(*security.RunAsGroupIDRange), b.(*securityv1.RunAsGroupIDRange), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*securityv1.RunAsUserStrategyOptions)(nil), (*security.RunAsUserStrategyOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_RunAsUserStrategyOptions_To_security_RunAsUserStrategyOptions(a.(*securityv1.RunAsUserStrategyOptions), b.(*security.RunAsUserStrategyOptions), scope) }); err != nil { @@ -204,11 +214,31 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*security.RunAsGroupIDRange)(nil), (*securityv1.IDRange)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_security_RunAsGroupIDRange_To_v1_IDRange(a.(*security.RunAsGroupIDRange), b.(*securityv1.IDRange), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*security.RunAsGroupStrategyOptions)(nil), (*securityv1.RunAsGroupStrategyOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_security_RunAsGroupStrategyOptions_To_v1_RunAsGroupStrategyOptions(a.(*security.RunAsGroupStrategyOptions), b.(*securityv1.RunAsGroupStrategyOptions), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*security.SecurityContextConstraints)(nil), (*securityv1.SecurityContextConstraints)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_security_SecurityContextConstraints_To_v1_SecurityContextConstraints(a.(*security.SecurityContextConstraints), b.(*securityv1.SecurityContextConstraints), scope) }); err != nil { return err } + if err := s.AddConversionFunc((*securityv1.IDRange)(nil), (*security.RunAsGroupIDRange)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_IDRange_To_security_RunAsGroupIDRange(a.(*securityv1.IDRange), b.(*security.RunAsGroupIDRange), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*securityv1.RunAsGroupStrategyOptions)(nil), (*security.RunAsGroupStrategyOptions)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_RunAsGroupStrategyOptions_To_security_RunAsGroupStrategyOptions(a.(*securityv1.RunAsGroupStrategyOptions), b.(*security.RunAsGroupStrategyOptions), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*securityv1.SecurityContextConstraints)(nil), (*security.SecurityContextConstraints)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_SecurityContextConstraints_To_security_SecurityContextConstraints(a.(*securityv1.SecurityContextConstraints), b.(*security.SecurityContextConstraints), scope) }); err != nil { @@ -585,6 +615,46 @@ func Convert_security_RangeAllocationList_To_v1_RangeAllocationList(in *security return autoConvert_security_RangeAllocationList_To_v1_RangeAllocationList(in, out, s) } +func autoConvert_v1_RunAsGroupIDRange_To_security_RunAsGroupIDRange(in *securityv1.RunAsGroupIDRange, out *security.RunAsGroupIDRange, s conversion.Scope) error { + out.Min = (*int64)(unsafe.Pointer(in.Min)) + out.Max = (*int64)(unsafe.Pointer(in.Max)) + return nil +} + +// Convert_v1_RunAsGroupIDRange_To_security_RunAsGroupIDRange is an autogenerated conversion function. +func Convert_v1_RunAsGroupIDRange_To_security_RunAsGroupIDRange(in *securityv1.RunAsGroupIDRange, out *security.RunAsGroupIDRange, s conversion.Scope) error { + return autoConvert_v1_RunAsGroupIDRange_To_security_RunAsGroupIDRange(in, out, s) +} + +func autoConvert_security_RunAsGroupIDRange_To_v1_RunAsGroupIDRange(in *security.RunAsGroupIDRange, out *securityv1.RunAsGroupIDRange, s conversion.Scope) error { + out.Min = (*int64)(unsafe.Pointer(in.Min)) + out.Max = (*int64)(unsafe.Pointer(in.Max)) + return nil +} + +// Convert_security_RunAsGroupIDRange_To_v1_RunAsGroupIDRange is an autogenerated conversion function. +func Convert_security_RunAsGroupIDRange_To_v1_RunAsGroupIDRange(in *security.RunAsGroupIDRange, out *securityv1.RunAsGroupIDRange, s conversion.Scope) error { + return autoConvert_security_RunAsGroupIDRange_To_v1_RunAsGroupIDRange(in, out, s) +} + +func autoConvert_v1_RunAsGroupStrategyOptions_To_security_RunAsGroupStrategyOptions(in *securityv1.RunAsGroupStrategyOptions, out *security.RunAsGroupStrategyOptions, s conversion.Scope) error { + out.Type = security.RunAsGroupStrategyType(in.Type) + out.GID = (*int64)(unsafe.Pointer(in.GID)) + out.GIDRangeMin = (*int64)(unsafe.Pointer(in.GIDRangeMin)) + out.GIDRangeMax = (*int64)(unsafe.Pointer(in.GIDRangeMax)) + out.Ranges = *(*[]security.RunAsGroupIDRange)(unsafe.Pointer(&in.Ranges)) + return nil +} + +func autoConvert_security_RunAsGroupStrategyOptions_To_v1_RunAsGroupStrategyOptions(in *security.RunAsGroupStrategyOptions, out *securityv1.RunAsGroupStrategyOptions, s conversion.Scope) error { + out.Type = securityv1.RunAsGroupStrategyType(in.Type) + out.GID = (*int64)(unsafe.Pointer(in.GID)) + out.GIDRangeMin = (*int64)(unsafe.Pointer(in.GIDRangeMin)) + out.GIDRangeMax = (*int64)(unsafe.Pointer(in.GIDRangeMax)) + out.Ranges = *(*[]securityv1.RunAsGroupIDRange)(unsafe.Pointer(&in.Ranges)) + return nil +} + func autoConvert_v1_RunAsUserStrategyOptions_To_security_RunAsUserStrategyOptions(in *securityv1.RunAsUserStrategyOptions, out *security.RunAsUserStrategyOptions, s conversion.Scope) error { out.Type = security.RunAsUserStrategyType(in.Type) out.UID = (*int64)(unsafe.Pointer(in.UID)) @@ -678,6 +748,9 @@ func autoConvert_v1_SecurityContextConstraints_To_security_SecurityContextConstr if err := Convert_v1_FSGroupStrategyOptions_To_security_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, s); err != nil { return err } + if err := Convert_v1_RunAsGroupStrategyOptions_To_security_RunAsGroupStrategyOptions(&in.RunAsGroup, &out.RunAsGroup, s); err != nil { + return err + } out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem out.Users = *(*[]string)(unsafe.Pointer(&in.Users)) out.Groups = *(*[]string)(unsafe.Pointer(&in.Groups)) @@ -714,6 +787,9 @@ func autoConvert_security_SecurityContextConstraints_To_v1_SecurityContextConstr if err := Convert_security_FSGroupStrategyOptions_To_v1_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, s); err != nil { return err } + if err := Convert_security_RunAsGroupStrategyOptions_To_v1_RunAsGroupStrategyOptions(&in.RunAsGroup, &out.RunAsGroup, s); err != nil { + return err + } out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem out.SeccompProfiles = *(*[]string)(unsafe.Pointer(&in.SeccompProfiles)) out.Users = *(*[]string)(unsafe.Pointer(&in.Users)) diff --git a/pkg/security/apis/security/validation/validation.go b/pkg/security/apis/security/validation/validation.go index 93aaa35b7..c6dc35b1d 100644 --- a/pkg/security/apis/security/validation/validation.go +++ b/pkg/security/apis/security/validation/validation.go @@ -72,6 +72,48 @@ func ValidateSecurityContextConstraints(scc *securityapi.SecurityContextConstrai } allErrs = append(allErrs, validateIDRanges(scc.SupplementalGroups.Ranges, field.NewPath("supplementalGroups"))...) + // validate runAsGroup if present + if scc.RunAsGroup.Type != "" { + runAsGroupPath := field.NewPath("runAsGroup") + switch scc.RunAsGroup.Type { + case securityapi.RunAsGroupStrategyMustRunAs, securityapi.RunAsGroupStrategyMustRunAsRange, securityapi.RunAsGroupStrategyRunAsAny: + // good types + default: + msg := fmt.Sprintf("invalid strategy type. Valid values are %s, %s, %s", + securityapi.RunAsGroupStrategyMustRunAs, + securityapi.RunAsGroupStrategyMustRunAsRange, + securityapi.RunAsGroupStrategyRunAsAny) + allErrs = append(allErrs, field.Invalid(runAsGroupPath.Child("type"), scc.RunAsGroup.Type, msg)) + } + + // if specified, gid cannot be negative + if scc.RunAsGroup.GID != nil { + if *scc.RunAsGroup.GID < 0 { + allErrs = append(allErrs, field.Invalid(runAsGroupPath.Child("gid"), *scc.RunAsGroup.GID, "gid cannot be negative")) + } + } + + // validate GID range if specified + if scc.RunAsGroup.GIDRangeMin != nil { + if *scc.RunAsGroup.GIDRangeMin < 0 { + allErrs = append(allErrs, field.Invalid(runAsGroupPath.Child("gidRangeMin"), *scc.RunAsGroup.GIDRangeMin, "gidRangeMin cannot be negative")) + } + } + if scc.RunAsGroup.GIDRangeMax != nil { + if *scc.RunAsGroup.GIDRangeMax < 0 { + allErrs = append(allErrs, field.Invalid(runAsGroupPath.Child("gidRangeMax"), *scc.RunAsGroup.GIDRangeMax, "gidRangeMax cannot be negative")) + } + } + if scc.RunAsGroup.GIDRangeMin != nil && scc.RunAsGroup.GIDRangeMax != nil { + if *scc.RunAsGroup.GIDRangeMin > *scc.RunAsGroup.GIDRangeMax { + allErrs = append(allErrs, field.Invalid(runAsGroupPath.Child("gidRangeMin"), scc.RunAsGroup, "gidRangeMin cannot be greater than gidRangeMax")) + } + } + + // validate ranges if specified + allErrs = append(allErrs, validateRunAsGroupIDRanges(scc.RunAsGroup.Ranges, runAsGroupPath)...) + } + // validate capabilities allErrs = append(allErrs, validateSCCCapsAgainstDrops(scc.RequiredDropCapabilities, scc.DefaultAddCapabilities, field.NewPath("defaultAddCapabilities"))...) allErrs = append(allErrs, validateSCCCapsAgainstDrops(scc.RequiredDropCapabilities, scc.AllowedCapabilities, field.NewPath("allowedCapabilities"))...) @@ -270,6 +312,36 @@ func validateIDRanges(rng []securityapi.IDRange, fldPath *field.Path) field.Erro return allErrs } +// validateRunAsGroupIDRanges ensures the RunAsGroupIDRange is valid. +func validateRunAsGroupIDRanges(rng []securityapi.RunAsGroupIDRange, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + for i, r := range rng { + // if 0 <= Min <= Max then we do not need to validate max. It is always greater than or + // equal to 0 and Min. + minPath := fldPath.Child("ranges").Index(i).Child("min") + maxPath := fldPath.Child("ranges").Index(i).Child("max") + + if r.Min != nil { + if *r.Min < 0 { + allErrs = append(allErrs, field.Invalid(minPath, *r.Min, "min cannot be negative")) + } + } + if r.Max != nil { + if *r.Max < 0 { + allErrs = append(allErrs, field.Invalid(maxPath, *r.Max, "max cannot be negative")) + } + } + if r.Min != nil && r.Max != nil { + if *r.Min > *r.Max { + allErrs = append(allErrs, field.Invalid(minPath, r, "min cannot be greater than max")) + } + } + } + + return allErrs +} + func ValidateSecurityContextConstraintsUpdate(newScc, oldScc *securityapi.SecurityContextConstraints) field.ErrorList { allErrs := validation.ValidateObjectMetaUpdate(&newScc.ObjectMeta, &oldScc.ObjectMeta, field.NewPath("metadata")) allErrs = append(allErrs, ValidateSecurityContextConstraints(newScc)...) diff --git a/pkg/security/apis/security/validation/validation_test.go b/pkg/security/apis/security/validation/validation_test.go index 2e76813c9..2fe133302 100644 --- a/pkg/security/apis/security/validation/validation_test.go +++ b/pkg/security/apis/security/validation/validation_test.go @@ -124,6 +124,56 @@ func TestValidateSecurityContextConstraints(t *testing.T) { invalidDuplicatedSysctls.ForbiddenSysctls = []string{"net.ipv4.ip_local_port_range"} invalidDuplicatedSysctls.AllowedUnsafeSysctls = []string{"net.ipv4.ip_local_port_range"} + // RunAsGroup test cases + invalidRunAsGroupStratType := validSCC() + invalidRunAsGroupStratType.RunAsGroup.Type = "invalid" + + var invalidGID int64 = -1 + invalidGIDSCC := validSCC() + invalidGIDSCC.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAs + invalidGIDSCC.RunAsGroup.GID = &invalidGID + + var negativeGIDMin int64 = -1 + invalidGIDRangeMin := validSCC() + invalidGIDRangeMin.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidGIDRangeMin.RunAsGroup.GIDRangeMin = &negativeGIDMin + + var negativeGIDMax int64 = -10 + invalidGIDRangeMax := validSCC() + invalidGIDRangeMax.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidGIDRangeMax.RunAsGroup.GIDRangeMax = &negativeGIDMax + + var gidMin int64 = 2000 + var gidMax int64 = 1000 + invalidGIDRangeMinGreaterThanMax := validSCC() + invalidGIDRangeMinGreaterThanMax.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidGIDRangeMinGreaterThanMax.RunAsGroup.GIDRangeMin = &gidMin + invalidGIDRangeMinGreaterThanMax.RunAsGroup.GIDRangeMax = &gidMax + + var rangeMin2 int64 = 2 + var rangeMax1 int64 = 1 + invalidRunAsGroupRangeMinGreaterThanMax := validSCC() + invalidRunAsGroupRangeMinGreaterThanMax.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidRunAsGroupRangeMinGreaterThanMax.RunAsGroup.Ranges = []securityapi.RunAsGroupIDRange{ + {Min: &rangeMin2, Max: &rangeMax1}, + } + + var rangeMinNeg int64 = -1 + var rangeMax10 int64 = 10 + invalidRunAsGroupRangeNegativeMin := validSCC() + invalidRunAsGroupRangeNegativeMin.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidRunAsGroupRangeNegativeMin.RunAsGroup.Ranges = []securityapi.RunAsGroupIDRange{ + {Min: &rangeMinNeg, Max: &rangeMax10}, + } + + var rangeMin1 int64 = 1 + var rangeMaxNeg int64 = -10 + invalidRunAsGroupRangeNegativeMax := validSCC() + invalidRunAsGroupRangeNegativeMax.RunAsGroup.Type = securityapi.RunAsGroupStrategyMustRunAsRange + invalidRunAsGroupRangeNegativeMax.RunAsGroup.Ranges = []securityapi.RunAsGroupIDRange{ + {Min: &rangeMin1, Max: &rangeMaxNeg}, + } + errorCases := map[string]struct { scc *securityapi.SecurityContextConstraints errorType field.ErrorType @@ -249,6 +299,46 @@ func TestValidateSecurityContextConstraints(t *testing.T) { errorType: field.ErrorTypeInvalid, errorDetail: fmt.Sprintf("sysctl overlaps with %s", invalidDuplicatedSysctls.AllowedUnsafeSysctls[0]), }, + "invalid runAsGroup strategy type": { + scc: invalidRunAsGroupStratType, + errorType: field.ErrorTypeInvalid, + errorDetail: "invalid strategy type. Valid values are MustRunAs, MustRunAsRange, RunAsAny", + }, + "invalid gid": { + scc: invalidGIDSCC, + errorType: field.ErrorTypeInvalid, + errorDetail: "gid cannot be negative", + }, + "invalid gid range min": { + scc: invalidGIDRangeMin, + errorType: field.ErrorTypeInvalid, + errorDetail: "gidRangeMin cannot be negative", + }, + "invalid gid range max": { + scc: invalidGIDRangeMax, + errorType: field.ErrorTypeInvalid, + errorDetail: "gidRangeMax cannot be negative", + }, + "invalid gid range min greater than max": { + scc: invalidGIDRangeMinGreaterThanMax, + errorType: field.ErrorTypeInvalid, + errorDetail: "gidRangeMin cannot be greater than gidRangeMax", + }, + "invalid runAsGroup ranges min greater than max": { + scc: invalidRunAsGroupRangeMinGreaterThanMax, + errorType: field.ErrorTypeInvalid, + errorDetail: "min cannot be greater than max", + }, + "invalid runAsGroup ranges negative min": { + scc: invalidRunAsGroupRangeNegativeMin, + errorType: field.ErrorTypeInvalid, + errorDetail: "min cannot be negative", + }, + "invalid runAsGroup ranges negative max": { + scc: invalidRunAsGroupRangeNegativeMax, + errorType: field.ErrorTypeInvalid, + errorDetail: "max cannot be negative", + }, } for k, v := range errorCases { diff --git a/pkg/security/apis/security/zz_generated.deepcopy.go b/pkg/security/apis/security/zz_generated.deepcopy.go index 7eccc714f..e07e1d598 100644 --- a/pkg/security/apis/security/zz_generated.deepcopy.go +++ b/pkg/security/apis/security/zz_generated.deepcopy.go @@ -317,6 +317,70 @@ func (in *RangeAllocationList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunAsGroupIDRange) DeepCopyInto(out *RunAsGroupIDRange) { + *out = *in + if in.Min != nil { + in, out := &in.Min, &out.Min + *out = new(int64) + **out = **in + } + if in.Max != nil { + in, out := &in.Max, &out.Max + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunAsGroupIDRange. +func (in *RunAsGroupIDRange) DeepCopy() *RunAsGroupIDRange { + if in == nil { + return nil + } + out := new(RunAsGroupIDRange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunAsGroupStrategyOptions) DeepCopyInto(out *RunAsGroupStrategyOptions) { + *out = *in + if in.GID != nil { + in, out := &in.GID, &out.GID + *out = new(int64) + **out = **in + } + if in.GIDRangeMin != nil { + in, out := &in.GIDRangeMin, &out.GIDRangeMin + *out = new(int64) + **out = **in + } + if in.GIDRangeMax != nil { + in, out := &in.GIDRangeMax, &out.GIDRangeMax + *out = new(int64) + **out = **in + } + if in.Ranges != nil { + in, out := &in.Ranges, &out.Ranges + *out = make([]RunAsGroupIDRange, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunAsGroupStrategyOptions. +func (in *RunAsGroupStrategyOptions) DeepCopy() *RunAsGroupStrategyOptions { + if in == nil { + return nil + } + out := new(RunAsGroupStrategyOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RunAsUserStrategyOptions) DeepCopyInto(out *RunAsUserStrategyOptions) { *out = *in @@ -418,6 +482,7 @@ func (in *SecurityContextConstraints) DeepCopyInto(out *SecurityContextConstrain in.RunAsUser.DeepCopyInto(&out.RunAsUser) in.SupplementalGroups.DeepCopyInto(&out.SupplementalGroups) in.FSGroup.DeepCopyInto(&out.FSGroup) + in.RunAsGroup.DeepCopyInto(&out.RunAsGroup) if in.SeccompProfiles != nil { in, out := &in.SeccompProfiles, &out.SeccompProfiles *out = make([]string, len(*in)) diff --git a/vendor/github.com/openshift/api/security/v1/generated.proto b/vendor/github.com/openshift/api/security/v1/generated.proto index 933de5450..2aea9b683 100644 --- a/vendor/github.com/openshift/api/security/v1/generated.proto +++ b/vendor/github.com/openshift/api/security/v1/generated.proto @@ -179,6 +179,47 @@ message RangeAllocationList { repeated RangeAllocation items = 2; } +// RunAsGroupIDRange provides a min/max of an allowed range of group IDs for RunAsGroup strategy. +message RunAsGroupIDRange { + // min is the start of the range, inclusive. + // +required + optional int64 min = 1; + + // max is the end of the range, inclusive. + // +required + optional int64 max = 2; +} + +// RunAsGroupStrategyOptions defines the strategy type and options used to create the strategy. +message RunAsGroupStrategyOptions { + // type is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + // Valid values are "MustRunAs", "MustRunAsRange", and "RunAsAny". + // +required + // +kubebuilder:validation:Enum=MustRunAs;MustRunAsRange;RunAsAny + optional string type = 1; + + // gid is the group id that containers must run as. Required for the MustRunAs strategy if not using + // namespace/service account allocated gids. + // +optional + optional int64 gid = 2; + + // gidRangeMin defines the min value for a strategy that allocates by range. + // +optional + optional int64 gidRangeMin = 3; + + // gidRangeMax defines the max value for a strategy that allocates by range. + // +optional + optional int64 gidRangeMax = 4; + + // ranges are the allowed ranges of gids. If you would like to force a single + // gid then supply a single range with the same start and end. + // When omitted, any gid is allowed (equivalent to RunAsAny strategy). + // +optional + // +listType=atomic + // +kubebuilder:validation:MaxItems=256 + repeated RunAsGroupIDRange ranges = 5; +} + // RunAsUserStrategyOptions defines the strategy type and any options used to create the strategy. message RunAsUserStrategyOptions { // type is the strategy that will dictate what RunAsUser is used in the SecurityContext. @@ -221,6 +262,7 @@ message SELinuxContextStrategyOptions { // +kubebuilder:printcolumn:name="SELinux",type=string,JSONPath=.seLinuxContext.type,description="Strategy that will dictate what labels will be set in the SecurityContext" // +kubebuilder:printcolumn:name="RunAsUser",type=string,JSONPath=.runAsUser.type,description="Strategy that will dictate what RunAsUser is used in the SecurityContext" // +kubebuilder:printcolumn:name="FSGroup",type=string,JSONPath=.fsGroup.type,description="Strategy that will dictate what fs group is used by the SecurityContext" +// +kubebuilder:printcolumn:name="RunAsGroup",type=string,JSONPath=.runAsGroup.type,description="Strategy that will dictate what RunAsGroup is used by the SecurityContext" // +kubebuilder:printcolumn:name="SupGroup",type=string,JSONPath=.supplementalGroups.type,description="Strategy that will dictate what supplemental groups are used by the SecurityContext" // +kubebuilder:printcolumn:name="Priority",type=string,JSONPath=.priority,description="Sort order of SCCs" // +kubebuilder:printcolumn:name="ReadOnlyRootFS",type=string,JSONPath=.readOnlyRootFilesystem,description="Force containers to run with a read only root file system" @@ -337,6 +379,11 @@ message SecurityContextConstraints { // +nullable optional FSGroupStrategyOptions fsGroup = 16; + // runAsGroup is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + // When omitted, the RunAsGroup strategy will not be enforced and containers may run with any group ID. + // +optional + optional RunAsGroupStrategyOptions runAsGroup = 27; + // readOnlyRootFilesystem when set to true will force containers to run with a read only root file // system. If the container specifically requests to run with a non-read only root file system // the SCC should deny the pod. diff --git a/vendor/github.com/openshift/api/security/v1/types.go b/vendor/github.com/openshift/api/security/v1/types.go index fb491480d..4e627d194 100644 --- a/vendor/github.com/openshift/api/security/v1/types.go +++ b/vendor/github.com/openshift/api/security/v1/types.go @@ -31,6 +31,7 @@ var AllowAllCapabilities corev1.Capability = "*" // +kubebuilder:printcolumn:name="SELinux",type=string,JSONPath=.seLinuxContext.type,description="Strategy that will dictate what labels will be set in the SecurityContext" // +kubebuilder:printcolumn:name="RunAsUser",type=string,JSONPath=.runAsUser.type,description="Strategy that will dictate what RunAsUser is used in the SecurityContext" // +kubebuilder:printcolumn:name="FSGroup",type=string,JSONPath=.fsGroup.type,description="Strategy that will dictate what fs group is used by the SecurityContext" +// +kubebuilder:printcolumn:name="RunAsGroup",type=string,JSONPath=.runAsGroup.type,description="Strategy that will dictate what RunAsGroup is used by the SecurityContext" // +kubebuilder:printcolumn:name="SupGroup",type=string,JSONPath=.supplementalGroups.type,description="Strategy that will dictate what supplemental groups are used by the SecurityContext" // +kubebuilder:printcolumn:name="Priority",type=string,JSONPath=.priority,description="Sort order of SCCs" // +kubebuilder:printcolumn:name="ReadOnlyRootFS",type=string,JSONPath=.readOnlyRootFilesystem,description="Force containers to run with a read only root file system" @@ -131,6 +132,10 @@ type SecurityContextConstraints struct { // fsGroup is the strategy that will dictate what fs group is used by the SecurityContext. // +nullable FSGroup FSGroupStrategyOptions `json:"fsGroup,omitempty" protobuf:"bytes,16,opt,name=fsGroup"` + // runAsGroup is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + // When omitted, the RunAsGroup strategy will not be enforced and containers may run with any group ID. + // +optional + RunAsGroup RunAsGroupStrategyOptions `json:"runAsGroup,omitzero" protobuf:"bytes,27,opt,name=runAsGroup"` // readOnlyRootFilesystem when set to true will force containers to run with a read only root file // system. If the container specifically requests to run with a non-read only root file system // the SCC should deny the pod. @@ -268,6 +273,42 @@ type SupplementalGroupsStrategyOptions struct { Ranges []IDRange `json:"ranges,omitempty" protobuf:"bytes,2,rep,name=ranges"` } +// RunAsGroupStrategyOptions defines the strategy type and options used to create the strategy. +type RunAsGroupStrategyOptions struct { + // type is the strategy that will dictate what RunAsGroup is used in the SecurityContext. + // Valid values are "MustRunAs", "MustRunAsRange", and "RunAsAny". + // +required + // +kubebuilder:validation:Enum=MustRunAs;MustRunAsRange;RunAsAny + Type RunAsGroupStrategyType `json:"type,omitempty" protobuf:"bytes,1,opt,name=type,casttype=RunAsGroupStrategyType"` + // gid is the group id that containers must run as. Required for the MustRunAs strategy if not using + // namespace/service account allocated gids. + // +optional + GID *int64 `json:"gid,omitempty" protobuf:"varint,2,opt,name=gid"` + // gidRangeMin defines the min value for a strategy that allocates by range. + // +optional + GIDRangeMin *int64 `json:"gidRangeMin,omitempty" protobuf:"varint,3,opt,name=gidRangeMin"` + // gidRangeMax defines the max value for a strategy that allocates by range. + // +optional + GIDRangeMax *int64 `json:"gidRangeMax,omitempty" protobuf:"varint,4,opt,name=gidRangeMax"` + // ranges are the allowed ranges of gids. If you would like to force a single + // gid then supply a single range with the same start and end. + // When omitted, any gid is allowed (equivalent to RunAsAny strategy). + // +optional + // +listType=atomic + // +kubebuilder:validation:MaxItems=256 + Ranges []RunAsGroupIDRange `json:"ranges,omitempty" protobuf:"bytes,5,rep,name=ranges"` +} + +// RunAsGroupIDRange provides a min/max of an allowed range of group IDs for RunAsGroup strategy. +type RunAsGroupIDRange struct { + // min is the start of the range, inclusive. + // +required + Min *int64 `json:"min,omitempty" protobuf:"varint,1,opt,name=min"` + // max is the end of the range, inclusive. + // +required + Max *int64 `json:"max,omitempty" protobuf:"varint,2,opt,name=max"` +} + // IDRange provides a min/max of an allowed range of IDs. // TODO: this could be reused for UIDs. type IDRange struct { @@ -296,6 +337,10 @@ type SupplementalGroupsStrategyType string // SecurityContext type FSGroupStrategyType string +// RunAsGroupStrategyType denotes strategy types for generating RunAsGroup values for a +// SecurityContext +type RunAsGroupStrategyType string + const ( // NamespaceLevelAllowHost allows a pod to set `hostUsers` field to either `true` or `false` NamespaceLevelAllowHost NamespaceLevelType = "AllowHostLevel" @@ -321,6 +366,13 @@ const ( // container may make requests for any FSGroup labels. FSGroupStrategyRunAsAny FSGroupStrategyType = "RunAsAny" + // container must have RunAsGroup of X applied. + RunAsGroupStrategyMustRunAs RunAsGroupStrategyType = "MustRunAs" + // container must run with a gid in a range. + RunAsGroupStrategyMustRunAsRange RunAsGroupStrategyType = "MustRunAsRange" + // container may make requests for any RunAsGroup. + RunAsGroupStrategyRunAsAny RunAsGroupStrategyType = "RunAsAny" + // container must run as a particular gid. SupplementalGroupsStrategyMustRunAs SupplementalGroupsStrategyType = "MustRunAs" // container may make requests for any gid. diff --git a/vendor/github.com/openshift/api/security/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/security/v1/zz_generated.deepcopy.go index d6263fc02..7f7ca9435 100644 --- a/vendor/github.com/openshift/api/security/v1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/security/v1/zz_generated.deepcopy.go @@ -348,6 +348,58 @@ func (in *RunAsUserStrategyOptions) DeepCopy() *RunAsUserStrategyOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunAsGroupIDRange) DeepCopyInto(out *RunAsGroupIDRange) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunAsGroupIDRange. +func (in *RunAsGroupIDRange) DeepCopy() *RunAsGroupIDRange { + if in == nil { + return nil + } + out := new(RunAsGroupIDRange) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RunAsGroupStrategyOptions) DeepCopyInto(out *RunAsGroupStrategyOptions) { + *out = *in + if in.GID != nil { + in, out := &in.GID, &out.GID + *out = new(int64) + **out = **in + } + if in.GIDRangeMin != nil { + in, out := &in.GIDRangeMin, &out.GIDRangeMin + *out = new(int64) + **out = **in + } + if in.GIDRangeMax != nil { + in, out := &in.GIDRangeMax, &out.GIDRangeMax + *out = new(int64) + **out = **in + } + if in.Ranges != nil { + in, out := &in.Ranges, &out.Ranges + *out = make([]RunAsGroupIDRange, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RunAsGroupStrategyOptions. +func (in *RunAsGroupStrategyOptions) DeepCopy() *RunAsGroupStrategyOptions { + if in == nil { + return nil + } + out := new(RunAsGroupStrategyOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SELinuxContextStrategyOptions) DeepCopyInto(out *SELinuxContextStrategyOptions) { *out = *in diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/integration_verification_test.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/integration_verification_test.go new file mode 100644 index 000000000..a4d951404 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/integration_verification_test.go @@ -0,0 +1,371 @@ +package runasgroup + +import ( + "testing" + + securityv1 "github.com/openshift/api/security/v1" +) + +// TestAllStrategyImplementations verifies that all three runAsGroup strategies +// properly implement the RunAsGroupSecurityContextConstraintsStrategy interface +// and work correctly with the new RunAsGroupIDRange pointer types +func TestAllStrategyImplementations(t *testing.T) { + tests := []struct { + name string + strategyType string + opts *securityv1.RunAsGroupStrategyOptions + testGID *int64 + shouldPass bool + }{ + { + name: "MustRunAs with single GID - valid", + strategyType: "MustRunAs", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAs, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(1000)}, + }, + }, + testGID: int64Ptr(1000), + shouldPass: true, + }, + { + name: "MustRunAs with single GID - invalid", + strategyType: "MustRunAs", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAs, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(1000)}, + }, + }, + testGID: int64Ptr(2000), + shouldPass: false, + }, + { + name: "MustRunAsRange with single range - valid min", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + }, + testGID: int64Ptr(1000), + shouldPass: true, + }, + { + name: "MustRunAsRange with single range - valid mid", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + }, + testGID: int64Ptr(1500), + shouldPass: true, + }, + { + name: "MustRunAsRange with single range - valid max", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + }, + testGID: int64Ptr(2000), + shouldPass: true, + }, + { + name: "MustRunAsRange with single range - invalid below", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + }, + testGID: int64Ptr(999), + shouldPass: false, + }, + { + name: "MustRunAsRange with single range - invalid above", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + }, + testGID: int64Ptr(2001), + shouldPass: false, + }, + { + name: "MustRunAsRange with multiple ranges - valid in first", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + {Min: int64Ptr(5000), Max: int64Ptr(6000)}, + }, + }, + testGID: int64Ptr(1500), + shouldPass: true, + }, + { + name: "MustRunAsRange with multiple ranges - valid in second", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + {Min: int64Ptr(5000), Max: int64Ptr(6000)}, + }, + }, + testGID: int64Ptr(5500), + shouldPass: true, + }, + { + name: "MustRunAsRange with multiple ranges - invalid between ranges", + strategyType: "MustRunAsRange", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + {Min: int64Ptr(5000), Max: int64Ptr(6000)}, + }, + }, + testGID: int64Ptr(3000), + shouldPass: false, + }, + { + name: "RunAsAny - accepts any GID", + strategyType: "RunAsAny", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyRunAsAny, + }, + testGID: int64Ptr(9999), + shouldPass: true, + }, + { + name: "RunAsAny - accepts zero", + strategyType: "RunAsAny", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyRunAsAny, + }, + testGID: int64Ptr(0), + shouldPass: true, + }, + { + name: "RunAsAny - accepts nil", + strategyType: "RunAsAny", + opts: &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyRunAsAny, + }, + testGID: nil, + shouldPass: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var strategy RunAsGroupSecurityContextConstraintsStrategy + var err error + + // Create the appropriate strategy + switch tt.strategyType { + case "MustRunAs": + strategy, err = NewMustRunAs(tt.opts) + case "MustRunAsRange": + strategy, err = NewMustRunAsRange(tt.opts) + case "RunAsAny": + strategy, err = NewRunAsAny(tt.opts) + default: + t.Fatalf("Unknown strategy type: %s", tt.strategyType) + } + + if err != nil { + t.Fatalf("Failed to create strategy: %v", err) + } + + // Test Generate function + generated, err := strategy.Generate(nil, nil) + if err != nil { + t.Errorf("Generate failed: %v", err) + } + // For RunAsAny, generated should be nil + if tt.strategyType == "RunAsAny" && generated != nil { + t.Errorf("RunAsAny should generate nil, got %v", *generated) + } + // For MustRunAs and MustRunAsRange, generated should be non-nil + if tt.strategyType != "RunAsAny" && generated == nil { + t.Errorf("%s should generate a non-nil GID", tt.strategyType) + } + + // Test Validate function + errs := strategy.Validate(nil, nil, nil, tt.testGID) + hasErrors := len(errs) > 0 + + if tt.shouldPass && hasErrors { + t.Errorf("Validation should pass but got errors: %v", errs) + } + if !tt.shouldPass && !hasErrors { + t.Errorf("Validation should fail but got no errors") + } + }) + } +} + +// TestPointerHandling verifies that all strategies properly handle pointer types +// from RunAsGroupIDRange +func TestPointerHandling(t *testing.T) { + t.Run("MustRunAs handles pointer Min/Max correctly", func(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAs, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(100), Max: int64Ptr(100)}, + }, + } + strategy, err := NewMustRunAs(opts) + if err != nil { + t.Fatalf("NewMustRunAs failed: %v", err) + } + + generated, err := strategy.Generate(nil, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + if generated == nil { + t.Fatal("Generated GID should not be nil") + } + if *generated != 100 { + t.Errorf("Generated GID = %d, want 100", *generated) + } + }) + + t.Run("MustRunAsRange handles pointer Min/Max correctly", func(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(200), Max: int64Ptr(300)}, + }, + } + strategy, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("NewMustRunAsRange failed: %v", err) + } + + generated, err := strategy.Generate(nil, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + if generated == nil { + t.Fatal("Generated GID should not be nil") + } + if *generated != 200 { + t.Errorf("Generated GID = %d, want 200 (min of range)", *generated) + } + + // Test validation with GID in range + gid := int64Ptr(250) + errs := strategy.Validate(nil, nil, nil, gid) + if len(errs) > 0 { + t.Errorf("Validation failed for GID in range: %v", errs) + } + }) + + t.Run("validateIDRange handles nil pointers", func(t *testing.T) { + // Test with nil Min + rng := securityv1.RunAsGroupIDRange{Min: nil, Max: int64Ptr(100)} + err := validateIDRange(rng) + if err == nil { + t.Error("validateIDRange should fail with nil Min") + } + + // Test with nil Max + rng = securityv1.RunAsGroupIDRange{Min: int64Ptr(100), Max: nil} + err = validateIDRange(rng) + if err == nil { + t.Error("validateIDRange should fail with nil Max") + } + + // Test with both nil + rng = securityv1.RunAsGroupIDRange{Min: nil, Max: nil} + err = validateIDRange(rng) + if err == nil { + t.Error("validateIDRange should fail with both nil") + } + }) +} + +// TestEdgeCases verifies edge cases and boundary conditions +func TestEdgeCases(t *testing.T) { + t.Run("Zero GID handling", func(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAs, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(0), Max: int64Ptr(0)}, + }, + } + strategy, err := NewMustRunAs(opts) + if err != nil { + t.Fatalf("NewMustRunAs failed with zero GID: %v", err) + } + + generated, err := strategy.Generate(nil, nil) + if err != nil { + t.Fatalf("Generate failed: %v", err) + } + if *generated != 0 { + t.Errorf("Generated GID = %d, want 0", *generated) + } + + gid := int64Ptr(0) + errs := strategy.Validate(nil, nil, nil, gid) + if len(errs) > 0 { + t.Errorf("Validation failed for zero GID: %v", errs) + } + }) + + t.Run("Large GID values", func(t *testing.T) { + var largeGID int64 = 2147483647 // Max int32 + opts := &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(largeGID), Max: int64Ptr(largeGID)}, + }, + } + strategy, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("NewMustRunAsRange failed with large GID: %v", err) + } + + gid := int64Ptr(largeGID) + errs := strategy.Validate(nil, nil, nil, gid) + if len(errs) > 0 { + t.Errorf("Validation failed for large GID: %v", errs) + } + }) + + t.Run("Negative GID values (allowed)", func(t *testing.T) { + var negGID int64 = -1 + opts := &securityv1.RunAsGroupStrategyOptions{ + Type: securityv1.RunAsGroupStrategyMustRunAsRange, + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(-10), Max: int64Ptr(10)}, + }, + } + strategy, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("NewMustRunAsRange failed with negative GIDs: %v", err) + } + + gid := int64Ptr(negGID) + errs := strategy.Validate(nil, nil, nil, gid) + if len(errs) > 0 { + t.Errorf("Validation failed for negative GID in valid range: %v", errs) + } + }) +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas.go new file mode 100644 index 000000000..93555d830 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas.go @@ -0,0 +1,61 @@ +package runasgroup + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/util/validation/field" + api "k8s.io/kubernetes/pkg/apis/core" + + securityv1 "github.com/openshift/api/security/v1" +) + +// mustRunAs implements the RunAsGroupSecurityContextConstraintsStrategy interface +type mustRunAs struct { + opts *securityv1.RunAsGroupStrategyOptions +} + +var _ RunAsGroupSecurityContextConstraintsStrategy = &mustRunAs{} + +// NewMustRunAs provides a strategy that requires the container to run as a specific GID. +func NewMustRunAs(options *securityv1.RunAsGroupStrategyOptions) (RunAsGroupSecurityContextConstraintsStrategy, error) { + if options == nil { + return nil, fmt.Errorf("MustRunAs requires run as group options") + } + if len(options.Ranges) == 0 { + return nil, fmt.Errorf("MustRunAs requires at least one range") + } + // Validate the range is valid (min <= max) + if err := validateIDRange(options.Ranges[0]); err != nil { + return nil, fmt.Errorf("MustRunAs has invalid range: %v", err) + } + if *options.Ranges[0].Min != *options.Ranges[0].Max { + return nil, fmt.Errorf("MustRunAs requires the first range to have the same min and max GID") + } + return &mustRunAs{ + opts: options, + }, nil +} + +// Generate creates the gid based on policy rules. MustRunAs returns the GID it is initialized with. +func (s *mustRunAs) Generate(pod *api.Pod, container *api.Container) (*int64, error) { + return s.opts.Ranges[0].Min, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *mustRunAs) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, runAsGroup *int64) field.ErrorList { + allErrs := field.ErrorList{} + + if runAsGroup == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("runAsGroup"), "")) + return allErrs + } + + requiredGID := *s.opts.Ranges[0].Min + if requiredGID != *runAsGroup { + detail := fmt.Sprintf("must be in the ranges: [%d, %d]", requiredGID, requiredGID) + allErrs = append(allErrs, field.Invalid(fldPath.Child("runAsGroup"), *runAsGroup, detail)) + return allErrs + } + + return allErrs +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas_test.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas_test.go new file mode 100644 index 000000000..e7730dd4c --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunas_test.go @@ -0,0 +1,151 @@ +package runasgroup + +import ( + "fmt" + "strings" + "testing" + + securityv1 "github.com/openshift/api/security/v1" +) + +func TestMustRunAsOptions(t *testing.T) { + tests := map[string]struct { + opts *securityv1.RunAsGroupStrategyOptions + pass bool + }{ + "nil opts": { + opts: nil, + pass: false, + }, + "empty opts": { + opts: &securityv1.RunAsGroupStrategyOptions{}, + pass: false, + }, + "no ranges": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{}, + }, + pass: false, + }, + "range with different min and max": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1), Max: int64Ptr(10)}, + }, + }, + pass: false, + }, + "valid single GID (min == max)": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(5), Max: int64Ptr(5)}, + }, + }, + pass: true, + }, + "valid single GID zero": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(0), Max: int64Ptr(0)}, + }, + }, + pass: true, + }, + } + for name, tc := range tests { + _, err := NewMustRunAs(tc.opts) + if err != nil && tc.pass { + t.Errorf("%s expected to pass but received error %v", name, err) + } + if err == nil && !tc.pass { + t.Errorf("%s expected to fail but did not receive an error", name) + } + } +} + +func TestMustRunAsGenerate(t *testing.T) { + var gid int64 = 100 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gid), Max: int64Ptr(gid)}, + }, + } + mustRunAs, err := NewMustRunAs(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAs %v", err) + } + generated, err := mustRunAs.Generate(nil, nil) + if err != nil { + t.Fatalf("unexpected error generating gid %v", err) + } + if generated == nil { + t.Fatal("expected generated gid but got nil") + } + if *generated != gid { + t.Errorf("generated gid %d does not equal configured gid %d", *generated, gid) + } +} + +func TestMustRunAsValidate(t *testing.T) { + var gid int64 = 100 + var badGID int64 = 200 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gid), Max: int64Ptr(gid)}, + }, + } + mustRunAs, err := NewMustRunAs(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAs %v", err) + } + + // Test nil runAsGroup + errs := mustRunAs.Validate(nil, nil, nil, nil) + expectedMessage := "runAsGroup: Required value" + if len(errs) == 0 { + t.Errorf("expected errors from nil runAsGroup but got none") + } else if !strings.Contains(errs[0].Error(), expectedMessage) { + t.Errorf("expected error to contain %q but it did not: %v", expectedMessage, errs) + } + + // Test mismatched GID + errs = mustRunAs.Validate(nil, nil, nil, &badGID) + expectedMessage = fmt.Sprintf("runAsGroup: Invalid value: %d: must be in the ranges: [%d, %d]", badGID, gid, gid) + if len(errs) == 0 { + t.Errorf("expected errors from mismatch gid but got none") + } else if !strings.Contains(errs[0].Error(), expectedMessage) { + t.Errorf("expected error to contain %q but it did not: %v", expectedMessage, errs) + } + + // Test matching GID + errs = mustRunAs.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors from matching gid but got %v", errs) + } +} + +func TestMustRunAsValidateZeroGID(t *testing.T) { + var gid int64 = 0 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gid), Max: int64Ptr(gid)}, + }, + } + mustRunAs, err := NewMustRunAs(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAs with zero GID %v", err) + } + + // Test that zero GID is accepted when required + errs := mustRunAs.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors from matching zero gid but got %v", errs) + } + + // Test that non-zero GID is rejected + var nonZeroGID int64 = 1 + errs = mustRunAs.Validate(nil, nil, nil, &nonZeroGID) + if len(errs) == 0 { + t.Errorf("expected errors from non-zero gid when zero is required but got none") + } +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange.go new file mode 100644 index 000000000..135a87b30 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange.go @@ -0,0 +1,71 @@ +package runasgroup + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/util/validation/field" + api "k8s.io/kubernetes/pkg/apis/core" + + securityv1 "github.com/openshift/api/security/v1" +) + +// mustRunAsRange implements the RunAsGroupSecurityContextConstraintsStrategy interface +type mustRunAsRange struct { + opts *securityv1.RunAsGroupStrategyOptions +} + +var _ RunAsGroupSecurityContextConstraintsStrategy = &mustRunAsRange{} + +// NewMustRunAsRange provides a strategy that requires the container to run as a specific GID in a range. +func NewMustRunAsRange(options *securityv1.RunAsGroupStrategyOptions) (RunAsGroupSecurityContextConstraintsStrategy, error) { + if options == nil { + return nil, fmt.Errorf("MustRunAsRange requires run as group options") + } + if len(options.Ranges) == 0 { + return nil, fmt.Errorf("MustRunAsRange requires at least one range") + } + // Validate all ranges are valid (min <= max for each range) + for i, rng := range options.Ranges { + if err := validateIDRange(rng); err != nil { + return nil, fmt.Errorf("MustRunAsRange has invalid range at index %d: %v", i, err) + } + } + return &mustRunAsRange{ + opts: options, + }, nil +} + +// Generate creates the gid based on policy rules. MustRunAsRange returns the minimum GID of the first range. +func (s *mustRunAsRange) Generate(pod *api.Pod, container *api.Container) (*int64, error) { + return s.opts.Ranges[0].Min, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *mustRunAsRange) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, runAsGroup *int64) field.ErrorList { + allErrs := field.ErrorList{} + + if runAsGroup == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("runAsGroup"), "")) + return allErrs + } + + // Check if the GID falls within any of the allowed ranges + for _, rng := range s.opts.Ranges { + if *runAsGroup >= *rng.Min && *runAsGroup <= *rng.Max { + return allErrs + } + } + + // Build error message with all allowed ranges + rangesStr := "" + for i, rng := range s.opts.Ranges { + if i > 0 { + rangesStr += ", " + } + rangesStr += fmt.Sprintf("[%d, %d]", *rng.Min, *rng.Max) + } + detail := fmt.Sprintf("must be in the ranges: %s", rangesStr) + allErrs = append(allErrs, field.Invalid(fldPath.Child("runAsGroup"), *runAsGroup, detail)) + + return allErrs +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange_test.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange_test.go new file mode 100644 index 000000000..e685cf2ec --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/mustrunasrange_test.go @@ -0,0 +1,294 @@ +package runasgroup + +import ( + "fmt" + "strings" + "testing" + + securityv1 "github.com/openshift/api/security/v1" +) + +func TestMustRunAsRangeOptions(t *testing.T) { + tests := map[string]struct { + opts *securityv1.RunAsGroupStrategyOptions + pass bool + }{ + "nil opts": { + opts: nil, + pass: false, + }, + "empty opts": { + opts: &securityv1.RunAsGroupStrategyOptions{}, + pass: false, + }, + "no ranges": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{}, + }, + pass: false, + }, + "valid single range": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1), Max: int64Ptr(10)}, + }, + }, + pass: true, + }, + "valid single range with min == max": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(5), Max: int64Ptr(5)}, + }, + }, + pass: true, + }, + "valid multiple ranges": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1), Max: int64Ptr(10)}, + {Min: int64Ptr(100), Max: int64Ptr(200)}, + }, + }, + pass: true, + }, + "valid range starting at zero": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(0), Max: int64Ptr(100)}, + }, + }, + pass: true, + }, + } + for name, tc := range tests { + _, err := NewMustRunAsRange(tc.opts) + if err != nil && tc.pass { + t.Errorf("%s expected to pass but received error %v", name, err) + } + if err == nil && !tc.pass { + t.Errorf("%s expected to fail but did not receive an error", name) + } + } +} + +func TestMustRunAsRangeGenerate(t *testing.T) { + var gidMin int64 = 10 + var gidMax int64 = 20 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gidMin), Max: int64Ptr(gidMax)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange %v", err) + } + generated, err := mustRunAsRange.Generate(nil, nil) + if err != nil { + t.Fatalf("unexpected error generating gid %v", err) + } + if generated == nil { + t.Fatal("expected generated gid but got nil") + } + if *generated != gidMin { + t.Errorf("generated gid %d does not equal expected min gid %d", *generated, gidMin) + } +} + +func TestMustRunAsRangeGenerateMultipleRanges(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(20)}, + {Min: int64Ptr(100), Max: int64Ptr(200)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange %v", err) + } + generated, err := mustRunAsRange.Generate(nil, nil) + if err != nil { + t.Fatalf("unexpected error generating gid %v", err) + } + if generated == nil { + t.Fatal("expected generated gid but got nil") + } + // Should return the min of the first range + if *generated != 10 { + t.Errorf("generated gid %d does not equal expected min of first range 10", *generated) + } +} + +func TestMustRunAsRangeValidate(t *testing.T) { + var gidMin int64 = 10 + var gidMax int64 = 20 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gidMin), Max: int64Ptr(gidMax)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange %v", err) + } + + // Test nil runAsGroup + errs := mustRunAsRange.Validate(nil, nil, nil, nil) + expectedMessage := "runAsGroup: Required value" + if len(errs) == 0 { + t.Errorf("expected errors from nil runAsGroup but got none") + } else if !strings.Contains(errs[0].Error(), expectedMessage) { + t.Errorf("expected error to contain %q but it did not: %v", expectedMessage, errs) + } + + // Test GID below range + var lowGID int64 = 5 + errs = mustRunAsRange.Validate(nil, nil, nil, &lowGID) + expectedMessage = fmt.Sprintf("runAsGroup: Invalid value: %d: must be in the ranges: [%d, %d]", lowGID, gidMin, gidMax) + if len(errs) == 0 { + t.Errorf("expected errors from low gid but got none") + } else if !strings.Contains(errs[0].Error(), expectedMessage) { + t.Errorf("expected error to contain %q but it did not: %v", expectedMessage, errs) + } + + // Test GID above range + var highGID int64 = 25 + errs = mustRunAsRange.Validate(nil, nil, nil, &highGID) + expectedMessage = fmt.Sprintf("runAsGroup: Invalid value: %d: must be in the ranges: [%d, %d]", highGID, gidMin, gidMax) + if len(errs) == 0 { + t.Errorf("expected errors from high gid but got none") + } else if !strings.Contains(errs[0].Error(), expectedMessage) { + t.Errorf("expected error to contain %q but it did not: %v", expectedMessage, errs) + } + + // Test GID at minimum boundary + errs = mustRunAsRange.Validate(nil, nil, nil, &gidMin) + if len(errs) != 0 { + t.Errorf("expected no errors from min boundary gid but got %v", errs) + } + + // Test GID at maximum boundary + errs = mustRunAsRange.Validate(nil, nil, nil, &gidMax) + if len(errs) != 0 { + t.Errorf("expected no errors from max boundary gid but got %v", errs) + } + + // Test GID in middle of range + var midGID int64 = 15 + errs = mustRunAsRange.Validate(nil, nil, nil, &midGID) + if len(errs) != 0 { + t.Errorf("expected no errors from mid-range gid but got %v", errs) + } +} + +func TestMustRunAsRangeValidateMultipleRanges(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(20)}, + {Min: int64Ptr(100), Max: int64Ptr(200)}, + {Min: int64Ptr(1000), Max: int64Ptr(2000)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange %v", err) + } + + tests := []struct { + name string + gid int64 + shouldPass bool + description string + }{ + {"first range min", 10, true, "minimum of first range"}, + {"first range mid", 15, true, "middle of first range"}, + {"first range max", 20, true, "maximum of first range"}, + {"second range min", 100, true, "minimum of second range"}, + {"second range mid", 150, true, "middle of second range"}, + {"second range max", 200, true, "maximum of second range"}, + {"third range min", 1000, true, "minimum of third range"}, + {"third range mid", 1500, true, "middle of third range"}, + {"third range max", 2000, true, "maximum of third range"}, + {"before first range", 5, false, "before first range"}, + {"between first and second", 50, false, "between first and second range"}, + {"between second and third", 500, false, "between second and third range"}, + {"after third range", 3000, false, "after third range"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + errs := mustRunAsRange.Validate(nil, nil, nil, &tt.gid) + if tt.shouldPass && len(errs) != 0 { + t.Errorf("expected no errors for %s (gid %d) but got %v", tt.description, tt.gid, errs) + } + if !tt.shouldPass && len(errs) == 0 { + t.Errorf("expected errors for %s (gid %d) but got none", tt.description, tt.gid) + } + }) + } +} + +func TestMustRunAsRangeValidateErrorMessage(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(20)}, + {Min: int64Ptr(100), Max: int64Ptr(200)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange %v", err) + } + + var invalidGID int64 = 50 + errs := mustRunAsRange.Validate(nil, nil, nil, &invalidGID) + if len(errs) == 0 { + t.Fatal("expected error but got none") + } + + errorMsg := errs[0].Error() + // Error message should contain both ranges + if !strings.Contains(errorMsg, "[10, 20]") { + t.Errorf("expected error message to contain first range '[10, 20]' but got: %s", errorMsg) + } + if !strings.Contains(errorMsg, "[100, 200]") { + t.Errorf("expected error message to contain second range '[100, 200]' but got: %s", errorMsg) + } + if !strings.Contains(errorMsg, "must be in the ranges:") { + t.Errorf("expected error message to contain 'must be in the ranges:' but got: %s", errorMsg) + } +} + +func TestMustRunAsRangeSingleGIDRange(t *testing.T) { + // Test that a range with min == max works correctly + var gid int64 = 100 + opts := &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(gid), Max: int64Ptr(gid)}, + }, + } + mustRunAsRange, err := NewMustRunAsRange(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewMustRunAsRange with single GID %v", err) + } + + // Should accept the exact GID + errs := mustRunAsRange.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors for exact gid %d but got %v", gid, errs) + } + + // Should reject any other GID + var otherGID int64 = 99 + errs = mustRunAsRange.Validate(nil, nil, nil, &otherGID) + if len(errs) == 0 { + t.Errorf("expected errors for gid %d when only %d is allowed but got none", otherGID, gid) + } + + otherGID = 101 + errs = mustRunAsRange.Validate(nil, nil, nil, &otherGID) + if len(errs) == 0 { + t.Errorf("expected errors for gid %d when only %d is allowed but got none", otherGID, gid) + } +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany.go new file mode 100644 index 000000000..c3e901d05 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany.go @@ -0,0 +1,28 @@ +package runasgroup + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + api "k8s.io/kubernetes/pkg/apis/core" + + securityv1 "github.com/openshift/api/security/v1" +) + +// runAsAny implements the interface RunAsGroupSecurityContextConstraintsStrategy. +type runAsAny struct{} + +var _ RunAsGroupSecurityContextConstraintsStrategy = &runAsAny{} + +// NewRunAsAny provides a strategy that will return nil. +func NewRunAsAny(options *securityv1.RunAsGroupStrategyOptions) (RunAsGroupSecurityContextConstraintsStrategy, error) { + return &runAsAny{}, nil +} + +// Generate creates the gid based on policy rules. +func (s *runAsAny) Generate(pod *api.Pod, container *api.Container) (*int64, error) { + return nil, nil +} + +// Validate ensures that the specified values fall within the range of the strategy. +func (s *runAsAny) Validate(fldPath *field.Path, _ *api.Pod, _ *api.Container, runAsGroup *int64) field.ErrorList { + return field.ErrorList{} +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany_test.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany_test.go new file mode 100644 index 000000000..bb9e2f1c9 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/runasany_test.go @@ -0,0 +1,88 @@ +package runasgroup + +import ( + "testing" + + securityv1 "github.com/openshift/api/security/v1" +) + +func TestRunAsAnyOptions(t *testing.T) { + tests := map[string]struct { + opts *securityv1.RunAsGroupStrategyOptions + pass bool + }{ + "nil opts": { + opts: nil, + pass: true, + }, + "empty opts": { + opts: &securityv1.RunAsGroupStrategyOptions{}, + pass: true, + }, + "opts with ranges": { + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(1), Max: int64Ptr(10)}, + }, + }, + pass: true, + }, + } + for name, tc := range tests { + _, err := NewRunAsAny(tc.opts) + if err != nil && tc.pass { + t.Errorf("%s expected to pass but received error %v", name, err) + } + if err == nil && !tc.pass { + t.Errorf("%s expected to fail but did not receive an error", name) + } + } +} + +func TestRunAsAnyGenerate(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{} + runAsAny, err := NewRunAsAny(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + generated, err := runAsAny.Generate(nil, nil) + if err != nil { + t.Fatalf("unexpected error generating gid %v", err) + } + if generated != nil { + t.Errorf("expected nil gid from RunAsAny but got %v", *generated) + } +} + +func TestRunAsAnyValidate(t *testing.T) { + opts := &securityv1.RunAsGroupStrategyOptions{} + runAsAny, err := NewRunAsAny(opts) + if err != nil { + t.Fatalf("unexpected error initializing NewRunAsAny %v", err) + } + + // RunAsAny should accept nil + errs := runAsAny.Validate(nil, nil, nil, nil) + if len(errs) != 0 { + t.Errorf("expected no errors from nil runAsGroup but got %v", errs) + } + + // RunAsAny should accept any GID + var gid int64 = 0 + errs = runAsAny.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors from gid 0 but got %v", errs) + } + + gid = 999 + errs = runAsAny.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors from gid 999 but got %v", errs) + } + + gid = -1 + errs = runAsAny.Validate(nil, nil, nil, &gid) + if len(errs) != 0 { + t.Errorf("expected no errors from negative gid but got %v", errs) + } +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/test_helpers.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/test_helpers.go new file mode 100644 index 000000000..31056c42f --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/test_helpers.go @@ -0,0 +1,7 @@ +package runasgroup + +// int64Ptr returns a pointer to an int64 +// This is a test helper function used across multiple test files +func int64Ptr(v int64) *int64 { + return &v +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/types.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/types.go new file mode 100644 index 000000000..fae151be0 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/types.go @@ -0,0 +1,14 @@ +package runasgroup + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + api "k8s.io/kubernetes/pkg/apis/core" +) + +// RunAsGroupSecurityContextConstraintsStrategy defines the interface for all gid constraint strategies. +type RunAsGroupSecurityContextConstraintsStrategy interface { + // Generate creates the gid based on policy rules. + Generate(pod *api.Pod, container *api.Container) (*int64, error) + // Validate ensures that the specified values fall within the range of the strategy. + Validate(fldPath *field.Path, pod *api.Pod, container *api.Container, runAsGroup *int64) field.ErrorList +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util.go new file mode 100644 index 000000000..a27a479a9 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util.go @@ -0,0 +1,24 @@ +package runasgroup + +import ( + "fmt" + + securityv1 "github.com/openshift/api/security/v1" +) + +// validateIDRange validates that a RunAsGroupIDRange has valid min/max values. +// It ensures that Min and Max are set and Min <= Max to prevent invalid range configurations. +func validateIDRange(rng securityv1.RunAsGroupIDRange) error { + if rng.Min == nil { + return fmt.Errorf("min must be specified") + } + if rng.Max == nil { + return fmt.Errorf("max must be specified") + } + if *rng.Min > *rng.Max { + return fmt.Errorf("min (%d) must be less than or equal to max (%d)", *rng.Min, *rng.Max) + } + // Note: Negative GID values are allowed as they may be used in some systems + // If validation for non-negative values is needed, it should be added here + return nil +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util_test.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util_test.go new file mode 100644 index 000000000..62a0a0ca2 --- /dev/null +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup/util_test.go @@ -0,0 +1,197 @@ +package runasgroup + +import ( + "strings" + "testing" + + securityv1 "github.com/openshift/api/security/v1" +) + +func TestValidateIDRange(t *testing.T) { + tests := []struct { + name string + rng securityv1.RunAsGroupIDRange + expectError bool + errorMsg string + }{ + { + name: "valid range with min < max", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(10), Max: int64Ptr(20)}, + expectError: false, + }, + { + name: "valid range with min == max", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(100), Max: int64Ptr(100)}, + expectError: false, + }, + { + name: "invalid range with min > max", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(200), Max: int64Ptr(100)}, + expectError: true, + errorMsg: "min (200) must be less than or equal to max (100)", + }, + { + name: "valid range with zero values", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(0), Max: int64Ptr(0)}, + expectError: false, + }, + { + name: "valid range starting at zero", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(0), Max: int64Ptr(1000)}, + expectError: false, + }, + { + name: "valid range with negative values (allowed)", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(-10), Max: int64Ptr(10)}, + expectError: false, + }, + { + name: "invalid range with negative max less than negative min", + rng: securityv1.RunAsGroupIDRange{Min: int64Ptr(-5), Max: int64Ptr(-10)}, + expectError: true, + errorMsg: "min (-5) must be less than or equal to max (-10)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateIDRange(tt.rng) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("expected error to contain %q but got %q", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} + +func TestMustRunAsWithInvalidRange(t *testing.T) { + tests := []struct { + name string + opts *securityv1.RunAsGroupStrategyOptions + expectError bool + errorMsg string + }{ + { + name: "invalid range with min > max", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(200), Max: int64Ptr(100)}, + }, + }, + expectError: true, + errorMsg: "invalid range", + }, + { + name: "valid range with min == max", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(100), Max: int64Ptr(100)}, + }, + }, + expectError: false, + }, + { + name: "invalid range with negative max less than min", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(-10)}, + }, + }, + expectError: true, + errorMsg: "invalid range", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewMustRunAs(tt.opts) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("expected error to contain %q but got %q", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} + +func TestMustRunAsRangeWithInvalidRanges(t *testing.T) { + tests := []struct { + name string + opts *securityv1.RunAsGroupStrategyOptions + expectError bool + errorMsg string + }{ + { + name: "single invalid range with min > max", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(200), Max: int64Ptr(100)}, + }, + }, + expectError: true, + errorMsg: "invalid range at index 0", + }, + { + name: "multiple ranges with one invalid", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(20)}, + {Min: int64Ptr(100), Max: int64Ptr(50)}, // Invalid + }, + }, + expectError: true, + errorMsg: "invalid range at index 1", + }, + { + name: "multiple valid ranges", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(10), Max: int64Ptr(20)}, + {Min: int64Ptr(100), Max: int64Ptr(200)}, + }, + }, + expectError: false, + }, + { + name: "all ranges invalid", + opts: &securityv1.RunAsGroupStrategyOptions{ + Ranges: []securityv1.RunAsGroupIDRange{ + {Min: int64Ptr(200), Max: int64Ptr(100)}, + {Min: int64Ptr(1000), Max: int64Ptr(500)}, + }, + }, + expectError: true, + errorMsg: "invalid range at index 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewMustRunAsRange(tt.opts) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("expected error to contain %q but got %q", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} diff --git a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching/provider.go b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching/provider.go index 6acd3913d..542cd703b 100644 --- a/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching/provider.go +++ b/vendor/github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching/provider.go @@ -13,6 +13,7 @@ import ( securityv1 "github.com/openshift/api/security/v1" "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/capabilities" "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/group" + "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup" "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/seccomp" "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/selinux" "github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sysctl" @@ -32,6 +33,7 @@ const ( type simpleProvider struct { scc *securityv1.SecurityContextConstraints runAsUserStrategy user.RunAsUserSecurityContextConstraintsStrategy + runAsGroupStrategy runasgroup.RunAsGroupSecurityContextConstraintsStrategy seLinuxStrategy selinux.SELinuxSecurityContextConstraintsStrategy fsGroupStrategy group.GroupSecurityContextConstraintsStrategy supplementalGroupStrategy group.GroupSecurityContextConstraintsStrategy @@ -54,6 +56,11 @@ func NewSimpleProvider(scc *securityv1.SecurityContextConstraints) (SecurityCont return nil, err } + groupStrat, err := createRunAsGroupStrategy(&scc.RunAsGroup) + if err != nil { + return nil, err + } + seLinuxStrat, err := createSELinuxStrategy(&scc.SELinuxContext) if err != nil { return nil, err @@ -87,6 +94,7 @@ func NewSimpleProvider(scc *securityv1.SecurityContextConstraints) (SecurityCont return &simpleProvider{ scc: scc, runAsUserStrategy: userStrat, + runAsGroupStrategy: groupStrat, seLinuxStrategy: seLinuxStrat, fsGroupStrategy: fsGroupStrat, supplementalGroupStrategy: supGroupStrat, @@ -161,6 +169,14 @@ func (s *simpleProvider) CreateContainerSecurityContext(pod *api.Pod, container sc.SetRunAsUser(uid) } + if sc.RunAsGroup() == nil { + gid, err := s.runAsGroupStrategy.Generate(pod, container) + if err != nil { + return nil, err + } + sc.SetRunAsGroup(gid) + } + if sc.SELinuxOptions() == nil { seLinux, err := s.seLinuxStrategy.Generate(pod, container) if err != nil { @@ -337,6 +353,7 @@ func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, containe sc := securitycontext.NewEffectiveContainerSecurityContextAccessor(podSC, securitycontext.NewContainerSecurityContextMutator(container.SecurityContext)) allErrs = append(allErrs, s.runAsUserStrategy.Validate(fldPath, pod, container, sc.RunAsNonRoot(), sc.RunAsUser())...) + allErrs = append(allErrs, s.runAsGroupStrategy.Validate(fldPath, pod, container, sc.RunAsGroup())...) allErrs = append(allErrs, s.seLinuxStrategy.Validate(fldPath.Child("seLinuxOptions"), pod, container, sc.SELinuxOptions())...) allErrs = append(allErrs, s.seccompStrategy.ValidateContainer(pod, container)...) @@ -433,6 +450,37 @@ func createUserStrategy(opts *securityv1.RunAsUserStrategyOptions) (user.RunAsUs } } +// createRunAsGroupStrategy creates a new group strategy. +func createRunAsGroupStrategy(opts *securityv1.RunAsGroupStrategyOptions) (runasgroup.RunAsGroupSecurityContextConstraintsStrategy, error) { + // If no strategy is specified, default to RunAsAny + if opts.Type == "" { + return runasgroup.NewRunAsAny(opts) + } + + switch opts.Type { + case securityv1.RunAsGroupStrategyMustRunAs: + // The MustRunAs strategy type can be used in two ways: + // 1. Single GID: exactly one range with min==max (enforces a specific GID) + // 2. Range(s): one or more ranges where GID can vary within the range(s) + // + // We use different implementations for these two cases: + // - NewMustRunAs: validates against a single required GID + // - NewMustRunAsRange: validates against one or more allowed ranges + if len(opts.Ranges) == 1 && opts.Ranges[0].Min != nil && opts.Ranges[0].Max != nil && *opts.Ranges[0].Min == *opts.Ranges[0].Max { + // Exactly one range with min==max means a single required GID + return runasgroup.NewMustRunAs(opts) + } + // Multiple ranges, or a single range with min!=max + return runasgroup.NewMustRunAsRange(opts) + case securityv1.RunAsGroupStrategyMustRunAsRange: + return runasgroup.NewMustRunAsRange(opts) + case securityv1.RunAsGroupStrategyRunAsAny: + return runasgroup.NewRunAsAny(opts) + default: + return nil, fmt.Errorf("Unrecognized RunAsGroup strategy type %s", opts.Type) + } +} + // createSELinuxStrategy creates a new selinux strategy. func createSELinuxStrategy(opts *securityv1.SELinuxContextStrategyOptions) (selinux.SELinuxSecurityContextConstraintsStrategy, error) { switch opts.Type { diff --git a/vendor/k8s.io/kubernetes/pkg/apis/core/v1/zz_generated.conversion.go b/vendor/k8s.io/kubernetes/pkg/apis/core/v1/zz_generated.conversion.go index d8baad0bd..d97152af2 100644 --- a/vendor/k8s.io/kubernetes/pkg/apis/core/v1/zz_generated.conversion.go +++ b/vendor/k8s.io/kubernetes/pkg/apis/core/v1/zz_generated.conversion.go @@ -1,5 +1,5 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated +//go:build !temporary_tag +// +build !temporary_tag /* Copyright The Kubernetes Authors. diff --git a/vendor/modules.txt b/vendor/modules.txt index 8547c7c65..e03d7b381 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -468,6 +468,7 @@ github.com/openshift/apiserver-library-go/pkg/configflags github.com/openshift/apiserver-library-go/pkg/labelselector github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/capabilities github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/group +github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/runasgroup github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccdefaults github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/sccmatching github.com/openshift/apiserver-library-go/pkg/securitycontextconstraints/seccomp