pkg/frontend/validate.go (193 lines of code) (raw):

package frontend // Copyright (c) Microsoft Corporation. // Licensed under the Apache License 2.0. import ( "context" "fmt" "net/http" "regexp" "strings" "github.com/Azure/go-autorest/autorest/azure" "github.com/coreos/go-semver/semver" "k8s.io/apimachinery/pkg/runtime/schema" "github.com/Azure/ARO-RP/pkg/api" "github.com/Azure/ARO-RP/pkg/api/validate" "github.com/Azure/ARO-RP/pkg/database/cosmosdb" utilnamespace "github.com/Azure/ARO-RP/pkg/util/namespace" ) func validateTerminalProvisioningState(state api.ProvisioningState) error { if state.IsTerminal() { return nil } return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeRequestNotAllowed, "", fmt.Sprintf("Request is not allowed in provisioningState '%s'.", state)) } func (f *frontend) getSubscriptionDocument(ctx context.Context, key string) (*api.SubscriptionDocument, error) { r, err := azure.ParseResourceID(key) if err != nil { return nil, err } dbSubscriptions, err := f.dbGroup.Subscriptions() if err != nil { return nil, err } doc, err := dbSubscriptions.Get(ctx, r.SubscriptionID) if cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) { return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidSubscriptionState, "", fmt.Sprintf("Request is not allowed in unregistered subscription '%s'.", r.SubscriptionID)) } return doc, err } func (f *frontend) validateSubscriptionState(ctx context.Context, path string, allowedStates ...api.SubscriptionState) (*api.SubscriptionDocument, error) { doc, err := f.getSubscriptionDocument(ctx, path) if err != nil { return nil, err } for _, allowedState := range allowedStates { if doc.Subscription.State == allowedState { return doc, nil } } return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidSubscriptionState, "", fmt.Sprintf("Request is not allowed in subscription in state '%s'.", doc.Subscription.State)) } // validateOpenShiftUniqueKey returns which unique key if causing a 412 error func (f *frontend) validateOpenShiftUniqueKey(ctx context.Context, doc *api.OpenShiftClusterDocument) error { dbOpenShiftClusters, err := f.dbGroup.OpenShiftClusters() if err != nil { return err } docs, err := dbOpenShiftClusters.GetByClientID(ctx, doc.PartitionKey, doc.ClientIDKey) if err != nil { return err } if docs.Count != 0 { clientIdOrMsi := "" value := "" if doc.OpenShiftCluster.UsesWorkloadIdentity() { clusterMsiResourceId, err := doc.OpenShiftCluster.ClusterMsiResourceId() if err != nil { return err } clientIdOrMsi = "user assigned identity" value = clusterMsiResourceId.String() } else { clientIdOrMsi = "service principal with client ID" value = doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID } return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeDuplicateClientID, "", fmt.Sprintf("The provided %s '%s' is already in use by a cluster.", clientIdOrMsi, value)) } docs, err = dbOpenShiftClusters.GetByClusterResourceGroupID(ctx, doc.PartitionKey, doc.ClusterResourceGroupIDKey) if err != nil { return err } if docs.Count != 0 { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeDuplicateResourceGroup, "", fmt.Sprintf("The provided resource group '%s' already contains a cluster.", doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID)) } return api.NewCloudError(http.StatusInternalServerError, api.CloudErrorCodeInternalServerError, "", "Internal server error.") } // rxKubernetesString is weaker than Kubernetes validation, but strong enough to // prevent mischief var rxKubernetesString = regexp.MustCompile(`(?i)^[-a-z0-9.]{0,255}$`) func validatePermittedClusterwideObjects(gvr schema.GroupVersionResource) bool { permittedGroups := map[string]bool{ "apiserver.openshift.io": true, "aro.openshift.io": true, "authorization.openshift.io": true, "certificates.k8s.io": true, "config.openshift.io": true, "console.openshift.io": true, "imageregistry.operator.openshift.io": true, "machine.openshift.io": true, "machineconfiguration.openshift.io": true, "operator.openshift.io": true, "rbac.authorization.k8s.io": true, "metrics.k8s.io": true, } permittedObjects := map[string]map[string]bool{ "": {"nodes": true}, } allowedResources, groupHasException := permittedObjects[gvr.Group] return permittedGroups[gvr.Group] || (groupHasException && allowedResources[gvr.Resource]) } func validateAdminKubernetesObjectsNonCustomer(method string, gvr schema.GroupVersionResource, namespace, name string) error { if gvr.Empty() { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided resource is invalid.") } if namespace == "" && !validatePermittedClusterwideObjects(gvr) { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", fmt.Sprintf("Access to cluster-scoped object '%v' is forbidden.", gvr)) } if !utilnamespace.IsOpenShiftNamespace(namespace) { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", fmt.Sprintf("Access to the provided namespace '%s' is forbidden.", namespace)) } return validateAdminKubernetesObjects(method, gvr, namespace, name) } func validateAdminKubernetesObjects(method string, gvr schema.GroupVersionResource, namespace, name string) error { if gvr.Empty() { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided resource is invalid.") } if gvr.Resource == "secrets" || gvr.Group == "oauth.openshift.io" { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", "Access to secrets is forbidden.") } if method != http.MethodGet && (gvr.Group == "rbac.authorization.k8s.io" || gvr.Group == "authorization.openshift.io") { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", "Write access to RBAC is forbidden.") } if !rxKubernetesString.MatchString(namespace) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided namespace '%s' is invalid.", namespace)) } if (method != http.MethodGet && name == "") || !rxKubernetesString.MatchString(name) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided name '%s' is invalid.", name)) } return nil } func validateAdminKubernetesObjectsForceDelete(groupKind string) error { if !strings.EqualFold(groupKind, "Pod") { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", fmt.Sprintf("Force deleting groupKind '%s' is forbidden.", groupKind)) } return nil } func validateAdminVMName(vmName string) error { if vmName == "" || !rxKubernetesString.MatchString(vmName) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided vmName '%s' is invalid.", vmName)) } return nil } func validateAdminKubernetesPodLogs(namespace, podName, containerName string) error { if podName == "" || !rxKubernetesString.MatchString(podName) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided pod name '%s' is invalid.", podName)) } if namespace == "" || !rxKubernetesString.MatchString(namespace) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided namespace '%s' is invalid.", namespace)) } // Checking if the namespace is an OpenShift namespace not a customer workload namespace. if !utilnamespace.IsOpenShiftNamespace(namespace) { return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", fmt.Sprintf("Access to the provided namespace '%s' is forbidden.", namespace)) } if containerName == "" || !rxKubernetesString.MatchString(containerName) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided container name '%s' is invalid.", containerName)) } return nil } // Azure resource name rules: // https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftnetwork var rxNetworkInterfaceName = regexp.MustCompile(`^[a-zA-Z0-9].*\w$`) func validateNetworkInterfaceName(nicName string) error { if nicName == "" || !rxNetworkInterfaceName.MatchString(nicName) { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided nicName '%s' is invalid.", nicName)) } return nil } func validateAdminMasterVMSize(vmSize string) error { // check to ensure that the target size is supported as a master size for k := range validate.SupportedVMSizesByRole(validate.VMRoleMaster) { if strings.EqualFold(string(k), vmSize) { return nil } } return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", fmt.Sprintf("The provided vmSize '%s' is unsupported for master.", vmSize)) } // validateInstallVersion validates the install version set in the clusterprofile.version // TODO convert this into static validation instead of this receiver function in the validation for frontend. func (f *frontend) validateInstallVersion(ctx context.Context, oc *api.OpenShiftCluster) error { f.ocpVersionsMu.RLock() // If this request is from an older API or the user did not specify // the version to install, use the default version. if oc.Properties.ClusterProfile.Version == "" { oc.Properties.ClusterProfile.Version = f.defaultOcpVersion } _, ok := f.enabledOcpVersions[oc.Properties.ClusterProfile.Version] f.ocpVersionsMu.RUnlock() _, err := semver.NewVersion(oc.Properties.ClusterProfile.Version) if !ok || err != nil { return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "properties.clusterProfile.version", fmt.Sprintf("The requested OpenShift version '%s' is invalid.", oc.Properties.ClusterProfile.Version)) } return nil }