|  | 
|  | 1 | +package internal | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"encoding/json" | 
|  | 5 | +	"github.com/operator-framework/api/pkg/validation/errors" | 
|  | 6 | +	interfaces "github.com/operator-framework/api/pkg/validation/interfaces" | 
|  | 7 | + | 
|  | 8 | +	policyv1beta1 "k8s.io/api/policy/v1beta1" | 
|  | 9 | +	rbacv1 "k8s.io/api/rbac/v1" | 
|  | 10 | +	schedulingv1 "k8s.io/api/scheduling/v1" | 
|  | 11 | +	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | 
|  | 12 | +) | 
|  | 13 | + | 
|  | 14 | +var ObjectValidator interfaces.Validator = interfaces.ValidatorFunc(validateObjects) | 
|  | 15 | + | 
|  | 16 | +const ( | 
|  | 17 | +	PodDisruptionBudgetKind     = "PodDisruptionBudget" | 
|  | 18 | +	PriorityClassKind           = "PriorityClass" | 
|  | 19 | +	RoleKind                    = "Role" | 
|  | 20 | +	ClusterRoleKind             = "ClusterRole" | 
|  | 21 | +	PodDisruptionBudgetAPIGroup = "policy" | 
|  | 22 | +	SCCAPIGroup                 = "security.openshift.io" | 
|  | 23 | +) | 
|  | 24 | + | 
|  | 25 | +// defaultSCCs is a map of the default Security Context Constraints present as of OpenShift 4.5. | 
|  | 26 | +// See https://docs.openshift.com/container-platform/4.5/authentication/managing-security-context-constraints.html#security-context-constraints-about_configuring-internal-oauth | 
|  | 27 | +var defaultSCCs = map[string]struct{}{ | 
|  | 28 | +	"privileged":       {}, | 
|  | 29 | +	"restricted":       {}, | 
|  | 30 | +	"anyuid":           {}, | 
|  | 31 | +	"hostaccess":       {}, | 
|  | 32 | +	"hostmount-anyuid": {}, | 
|  | 33 | +	"hostnetwork":      {}, | 
|  | 34 | +	"node-exporter":    {}, | 
|  | 35 | +	"nonroot":          {}, | 
|  | 36 | +} | 
|  | 37 | + | 
|  | 38 | +func validateObjects(objs ...interface{}) (results []errors.ManifestResult) { | 
|  | 39 | +	for _, obj := range objs { | 
|  | 40 | +		switch u := obj.(type) { | 
|  | 41 | +		case *unstructured.Unstructured: | 
|  | 42 | +			switch u.GroupVersionKind().Kind { | 
|  | 43 | +			case PodDisruptionBudgetKind: | 
|  | 44 | +				results = append(results, validatePDB(u)) | 
|  | 45 | +			case PriorityClassKind: | 
|  | 46 | +				results = append(results, validatePriorityClass(u)) | 
|  | 47 | +			case RoleKind: | 
|  | 48 | +				results = append(results, validateRBAC(u)) | 
|  | 49 | +			case ClusterRoleKind: | 
|  | 50 | +				results = append(results, validateRBAC(u)) | 
|  | 51 | +			} | 
|  | 52 | +		} | 
|  | 53 | +	} | 
|  | 54 | +	return results | 
|  | 55 | +} | 
|  | 56 | + | 
|  | 57 | +// validatePDB checks the PDB to ensure the minimum and maximum budgets are set to reasonable levels. | 
|  | 58 | +// See https://github.com/operator-framework/operator-lifecycle-manager/blob/master/doc/design/adding-pod-disruption-budgets.md#limitations-on-pod-disruption-budgets | 
|  | 59 | +func validatePDB(u *unstructured.Unstructured) (result errors.ManifestResult) { | 
|  | 60 | +	pdb := policyv1beta1.PodDisruptionBudget{} | 
|  | 61 | + | 
|  | 62 | +	b, err := u.MarshalJSON() | 
|  | 63 | +	if err != nil { | 
|  | 64 | +		result.Add(errors.ErrInvalidParse("error converting unstructured", err)) | 
|  | 65 | +		return | 
|  | 66 | +	} | 
|  | 67 | + | 
|  | 68 | +	err = json.Unmarshal(b, &pdb) | 
|  | 69 | +	if err != nil { | 
|  | 70 | +		result.Add(errors.ErrInvalidParse("error unmarshaling poddisruptionbudget", err)) | 
|  | 71 | +		return | 
|  | 72 | +	} | 
|  | 73 | + | 
|  | 74 | +	/* | 
|  | 75 | +	   maxUnavailable field cannot be set to 0 or 0%. | 
|  | 76 | +	   minAvailable field cannot be set to 100%. | 
|  | 77 | +	*/ | 
|  | 78 | + | 
|  | 79 | +	maxUnavailable := pdb.Spec.MaxUnavailable | 
|  | 80 | +	if maxUnavailable != nil && (maxUnavailable.IntVal == 0 || maxUnavailable.StrVal == "0%") { | 
|  | 81 | +		result.Add(errors.ErrInvalidObject(pdb, "maxUnavailable field cannot be set to 0 or 0%")) | 
|  | 82 | +	} | 
|  | 83 | + | 
|  | 84 | +	minAvailable := pdb.Spec.MinAvailable | 
|  | 85 | +	if minAvailable != nil && minAvailable.StrVal == "100%" { | 
|  | 86 | +		result.Add(errors.ErrInvalidObject(pdb, "minAvailable field cannot be set to 100%")) | 
|  | 87 | +	} | 
|  | 88 | + | 
|  | 89 | +	return | 
|  | 90 | +} | 
|  | 91 | + | 
|  | 92 | +// validatePriorityClass checks the PriorityClass object to ensure globalDefault is set to false. | 
|  | 93 | +// See https://github.com/operator-framework/operator-lifecycle-manager/blob/master/doc/design/adding-priority-classes.md | 
|  | 94 | +func validatePriorityClass(u *unstructured.Unstructured) (result errors.ManifestResult) { | 
|  | 95 | +	pc := schedulingv1.PriorityClass{} | 
|  | 96 | + | 
|  | 97 | +	b, err := u.MarshalJSON() | 
|  | 98 | +	if err != nil { | 
|  | 99 | +		result.Add(errors.ErrInvalidParse("error converting unstructured", err)) | 
|  | 100 | +		return | 
|  | 101 | +	} | 
|  | 102 | + | 
|  | 103 | +	err = json.Unmarshal(b, &pc) | 
|  | 104 | +	if err != nil { | 
|  | 105 | +		result.Add(errors.ErrInvalidParse("error unmarshaling priorityclass", err)) | 
|  | 106 | +		return | 
|  | 107 | +	} | 
|  | 108 | + | 
|  | 109 | +	if pc.GlobalDefault { | 
|  | 110 | +		result.Add(errors.ErrInvalidObject(pc, "globalDefault field cannot be set to true")) | 
|  | 111 | +	} | 
|  | 112 | + | 
|  | 113 | +	return | 
|  | 114 | +} | 
|  | 115 | + | 
|  | 116 | +func validateRBAC(u *unstructured.Unstructured) (result errors.ManifestResult) { | 
|  | 117 | +	var policyRules []rbacv1.PolicyRule | 
|  | 118 | + | 
|  | 119 | +	b, err := u.MarshalJSON() | 
|  | 120 | +	if err != nil { | 
|  | 121 | +		result.Add(errors.ErrInvalidParse("error converting unstructured", err)) | 
|  | 122 | +		return | 
|  | 123 | +	} | 
|  | 124 | + | 
|  | 125 | +	switch u.GroupVersionKind().Kind { | 
|  | 126 | +	case RoleKind: | 
|  | 127 | +		role := rbacv1.Role{} | 
|  | 128 | +		err = json.Unmarshal(b, &role) | 
|  | 129 | +		if err != nil { | 
|  | 130 | +			result.Add(errors.ErrInvalidParse("error unmarshaling role", err)) | 
|  | 131 | +			return | 
|  | 132 | +		} | 
|  | 133 | +		policyRules = role.Rules | 
|  | 134 | +	case ClusterRoleKind: | 
|  | 135 | +		clusterrole := rbacv1.ClusterRole{} | 
|  | 136 | +		err = json.Unmarshal(b, &clusterrole) | 
|  | 137 | +		if err != nil { | 
|  | 138 | +			result.Add(errors.ErrInvalidParse("error unmarshaling clusterrole", err)) | 
|  | 139 | +			return | 
|  | 140 | +		} | 
|  | 141 | +		policyRules = clusterrole.Rules | 
|  | 142 | +	} | 
|  | 143 | + | 
|  | 144 | +	return audit(policyRules) | 
|  | 145 | +} | 
|  | 146 | + | 
|  | 147 | +// audit checks the provided rbac policies against prescribed limitations. | 
|  | 148 | +// If permission is granted to create/modify a PDB, a warning is returned. | 
|  | 149 | +// If permission is granted to modify default SCCs in OpenShift, an error is returned. | 
|  | 150 | +func audit(policies []rbacv1.PolicyRule) (result errors.ManifestResult) { | 
|  | 151 | +	// check for permission to modify/create PDBs | 
|  | 152 | +	for _, rule := range policies { | 
|  | 153 | +		if contains(rule.APIGroups, PodDisruptionBudgetAPIGroup) && | 
|  | 154 | +			contains(rule.Resources, "poddisruptionbudgets") && | 
|  | 155 | +			contains(rule.Verbs, rbacv1.VerbAll, "create", "update", "patch") { | 
|  | 156 | +			result.Add(errors.WarnInvalidObject("RBAC includes permission to create/update poddisruptionbudgets, which could impact cluster stability", rule)) | 
|  | 157 | +		} | 
|  | 158 | +	} | 
|  | 159 | + | 
|  | 160 | +	// check sccs for modifying default known SCCs | 
|  | 161 | +	for _, rule := range policies { | 
|  | 162 | +		if contains(rule.APIGroups, SCCAPIGroup) && | 
|  | 163 | +			contains(rule.Resources, "securitycontextconstraints") && | 
|  | 164 | +			contains(rule.Verbs, rbacv1.VerbAll, "delete", "update", "patch") && | 
|  | 165 | +			containsDefaults(rule.ResourceNames, defaultSCCs) { | 
|  | 166 | +			result.Add(errors.ErrInvalidObject(rule, "RBAC includes permission to modify default securitycontextconstraints, which could impact cluster stability")) | 
|  | 167 | +		} | 
|  | 168 | +	} | 
|  | 169 | + | 
|  | 170 | +	return | 
|  | 171 | +} | 
|  | 172 | + | 
|  | 173 | +// contains returns true if at least one item is present in the array | 
|  | 174 | +func contains(slice []string, items ...string) bool { | 
|  | 175 | +	set := make(map[string]struct{}, len(slice)) | 
|  | 176 | +	for _, s := range slice { | 
|  | 177 | +		set[s] = struct{}{} | 
|  | 178 | +	} | 
|  | 179 | + | 
|  | 180 | +	for _, item := range items { | 
|  | 181 | +		if _, ok := set[item]; ok { | 
|  | 182 | +			return true | 
|  | 183 | +		} | 
|  | 184 | +	} | 
|  | 185 | + | 
|  | 186 | +	return false | 
|  | 187 | +} | 
|  | 188 | + | 
|  | 189 | +// containsDefaults returns true if at least one item is present as a key in the map | 
|  | 190 | +func containsDefaults(slice []string, defaults map[string]struct{}) bool { | 
|  | 191 | +	for _, s := range slice { | 
|  | 192 | +		if _, ok := defaults[s]; ok { | 
|  | 193 | +			return true | 
|  | 194 | +		} | 
|  | 195 | +	} | 
|  | 196 | +	return false | 
|  | 197 | +} | 
0 commit comments