/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ranger.plugin.model.validation; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.ranger.plugin.errors.ValidationErrorCode; import org.apache.ranger.plugin.model.RangerPolicy; import org.apache.ranger.plugin.model.RangerServiceDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerAccessTypeDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerEnumDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerEnumElementDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerPolicyConditionDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerResourceDef; import org.apache.ranger.plugin.model.RangerServiceDef.RangerServiceConfigDef; import org.apache.ranger.plugin.store.ServiceStore; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; public class RangerServiceDefValidator extends RangerValidator { private static final Log LOG = LogFactory.getLog(RangerServiceDefValidator.class); public RangerServiceDefValidator(ServiceStore store) { super(store); } public void validate(final RangerServiceDef serviceDef, final Action action) throws Exception { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.validate(%s, %s)", serviceDef, action)); } List<ValidationFailureDetails> failures = new ArrayList<>(); boolean valid = isValid(serviceDef, action, failures); String message = ""; try { if (!valid) { message = serializeFailures(failures); throw new Exception(message); } } finally { if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.validate(%s, %s): %s, reason[%s]", serviceDef, action, valid, message)); } } } boolean isValid(final Long id, final Action action, final List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug("==> RangerServiceDefValidator.isValid(" + id + ")"); } boolean valid = true; if (action != Action.DELETE) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_UNSUPPORTED_ACTION; failures.add(new ValidationFailureDetailsBuilder() .isAnInternalError() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(action)) .build()); valid = false; } else if (id == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_MISSING_FIELD; failures.add(new ValidationFailureDetailsBuilder() .field("id") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage("id")) .build()); valid = false; } else if (getServiceDef(id) == null) { if (LOG.isDebugEnabled()) { LOG.debug("No service found for id[" + id + "]! ok!"); } } if(LOG.isDebugEnabled()) { LOG.debug("<== RangerServiceDefValidator.isValid(" + id + "): " + valid); } return valid; } boolean isValid(final RangerServiceDef serviceDef, final Action action, final List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug("==> RangerServiceDefValidator.isValid(" + serviceDef + ")"); } if (!(action == Action.CREATE || action == Action.UPDATE)) { throw new IllegalArgumentException("isValid(RangerServiceDef, ...) is only supported for CREATE/UPDATE"); } boolean valid = true; if (serviceDef == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_NULL_SERVICE_DEF_OBJECT; failures.add(new ValidationFailureDetailsBuilder() .field("service def") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(action)) .build()); valid = false; } else { Long id = serviceDef.getId(); valid = isValidServiceDefId(id, action, failures) && valid; valid = isValidServiceDefName(serviceDef.getName(), id, action, failures) && valid; valid = isValidAccessTypes(serviceDef.getAccessTypes(), failures) && valid; if (isValidResources(serviceDef, failures)) { // Semantic check of resource graph can only be done if resources are "syntactically" valid valid = isValidResourceGraph(serviceDef, failures) && valid; } else { valid = false; } List<RangerEnumDef> enumDefs = serviceDef.getEnums(); if (isValidEnums(enumDefs, failures)) { // config def validation requires valid enums valid = isValidConfigs(serviceDef.getConfigs(), enumDefs, failures) && valid; } else { valid = false; } valid = isValidPolicyConditions(serviceDef.getPolicyConditions(), failures) && valid; } if(LOG.isDebugEnabled()) { LOG.debug("<== RangerServiceDefValidator.isValid(" + serviceDef + "): " + valid); } return valid; } boolean isValidServiceDefId(Long id, final Action action, final List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidServiceDefId(%s, %s, %s)", id, action, failures)); } boolean valid = true; if (action == Action.UPDATE) { // id is ignored for CREATE if (id == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_EMPTY_SERVICE_DEF_ID; failures.add(new ValidationFailureDetailsBuilder() .field("id") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage()) .build()); valid = false; } else if (getServiceDef(id) == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_INVALID_SERVICE_DEF_ID; failures.add(new ValidationFailureDetailsBuilder() .field("id") .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(id)) .build()); valid = false; } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidServiceDefId(%s, %s, %s): %s", id, action, failures, valid)); } return valid; } boolean isValidServiceDefName(String name, Long id, final Action action, final List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidServiceDefName(%s, %s, %s, %s)", name, id, action, failures)); } boolean valid = true; if (StringUtils.isBlank(name)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_INVALID_SERVICE_DEF_NAME; failures.add(new ValidationFailureDetailsBuilder() .field("name") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(name)) .build()); valid = false; } else { RangerServiceDef otherServiceDef = getServiceDef(name); if (otherServiceDef != null && action == Action.CREATE) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_SERVICE_DEF_NAME_CONFICT; failures.add(new ValidationFailureDetailsBuilder() .field("name") .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(name)) .build()); valid = false; } else if (otherServiceDef != null && !Objects.equals(id, otherServiceDef.getId())) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_ID_NAME_CONFLICT; failures.add(new ValidationFailureDetailsBuilder() .field("id/name") .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(name, otherServiceDef.getId())) .build()); valid = false; } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidServiceDefName(%s, %s, %s, %s): %s", name, id, action, failures, valid)); } return valid; } boolean isValidAccessTypes(final List<RangerAccessTypeDef> accessTypeDefs, final List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidAccessTypes(%s, %s)", accessTypeDefs, failures)); } boolean valid = true; if (CollectionUtils.isEmpty(accessTypeDefs)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_MISSING_FIELD; failures.add(new ValidationFailureDetailsBuilder() .field("access types") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage("access types")) .build()); valid = false; } else { List<RangerAccessTypeDef> defsWithImpliedGrants = new ArrayList<>(); Set<String> accessNames = new HashSet<>(); Set<Long> ids = new HashSet<>(); for (RangerAccessTypeDef def : accessTypeDefs) { String name = def.getName(); valid = isUnique(name, accessNames, "access type name", "access types", failures) && valid; valid = isUnique(def.getItemId(), ids, "access type itemId", "access types", failures) && valid; if (CollectionUtils.isNotEmpty(def.getImpliedGrants())) { defsWithImpliedGrants.add(def); } } // validate implied grants for (RangerAccessTypeDef def : defsWithImpliedGrants) { Collection<String> impliedGrants = getImpliedGrants(def); Set<String> unknownAccessTypes = Sets.difference(Sets.newHashSet(impliedGrants), accessNames); if (!unknownAccessTypes.isEmpty()) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_IMPLIED_GRANT_UNKNOWN_ACCESS_TYPE; failures.add(new ValidationFailureDetailsBuilder() .field("implied grants") .subField(unknownAccessTypes.iterator().next()) // we return just on item here. Message has all unknow items .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(impliedGrants, unknownAccessTypes)) .build()); valid = false; } // implied grant should not imply itself! String name = def.getName(); // note: this name could be null/blank/empty! if (impliedGrants.contains(name)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_IMPLIED_GRANT_IMPLIES_ITSELF; failures.add(new ValidationFailureDetailsBuilder() .field("implied grants") .subField(name) .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(impliedGrants, name)) .build()); valid = false; } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidAccessTypes(%s, %s): %s", accessTypeDefs, failures, valid)); } return valid; } boolean isValidPolicyConditions(List<RangerPolicyConditionDef> policyConditions, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidPolicyConditions(%s, %s)", policyConditions, failures)); } boolean valid = true; if (CollectionUtils.isEmpty(policyConditions)) { LOG.debug("Configs collection was null/empty! ok"); } else { Set<Long> ids = new HashSet<>(); Set<String> names = new HashSet<>(); for (RangerPolicyConditionDef conditionDef : policyConditions) { valid = isUnique(conditionDef.getItemId(), ids, "policy condition def itemId", "policy condition defs", failures) && valid; String name = conditionDef.getName(); valid = isUnique(name, names, "policy condition def name", "policy condition defs", failures) && valid; if (StringUtils.isBlank(conditionDef.getEvaluator())) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_POLICY_CONDITION_NULL_EVALUATOR; failures.add(new ValidationFailureDetailsBuilder() .field("policy condition def evaluator") .subField(name) .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(name)) .build()); valid = false; } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidPolicyConditions(%s, %s): %s", policyConditions, failures, valid)); } return valid; } boolean isValidConfigs(List<RangerServiceConfigDef> configs, List<RangerEnumDef> enumDefs, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidConfigs(%s, %s, %s)", configs, enumDefs, failures)); } boolean valid = true; if (CollectionUtils.isEmpty(configs)) { LOG.debug("Configs collection was null/empty! ok"); } else { Set<Long> ids = new HashSet<Long>(configs.size()); Set<String> names = new HashSet<String>(configs.size()); for (RangerServiceConfigDef aConfig : configs) { valid = isUnique(aConfig.getItemId(), ids, "config def itemId", "config defs", failures) && valid; String configName = aConfig.getName(); valid = isUnique(configName, names, "config def name", "config defs", failures) && valid; String type = aConfig.getType(); valid = isValidConfigType(type, configName, failures) && valid; if ("enum".equals(type)) { valid = isValidConfigOfEnumType(aConfig, enumDefs, failures) && valid; } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidConfigs(%s, %s, %s): %s", configs, enumDefs, failures, valid)); } return valid; } boolean isValidConfigOfEnumType(RangerServiceConfigDef configDef, List<RangerEnumDef> enumDefs, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidConfigOfEnumType(%s, %s, %s)", configDef, enumDefs, failures)); } boolean valid = true; if (!"enum".equals(configDef.getType())) { LOG.debug("ConfigDef wasn't of enum type!"); } else { Map<String, RangerEnumDef> enumDefsMap = getEnumDefMap(enumDefs); Set<String> enumTypes = enumDefsMap.keySet(); String subType = configDef.getSubType(); String configName = configDef.getName(); if (!enumTypes.contains(subType)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_CONFIG_DEF_UNKNOWN_ENUM; failures.add(new ValidationFailureDetailsBuilder() .field("config def subtype") .subField(configName) .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(subType, configName, enumTypes)) .build()); valid = false; } else { // default value check is possible only if sub-type is correctly configured String defaultValue = configDef.getDefaultValue(); if (StringUtils.isNotBlank(defaultValue)) { RangerEnumDef enumDef = enumDefsMap.get(subType); Set<String> enumValues = getEnumValues(enumDef); if (!enumValues.contains(defaultValue)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_CONFIG_DEF_UNKNOWN_ENUM_VALUE; failures.add(new ValidationFailureDetailsBuilder() .field("config def default value") .subField(configName) .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(defaultValue, configName, enumValues, subType)) .build()); valid = false; } } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidConfigOfEnumType(%s, %s, %s): %s", configDef, enumDefs, failures, valid)); } return valid; } boolean isValidConfigType(String type, String configName, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidConfigType(%s, %s, %s)", type, configName, failures)); } boolean valid = true; Set<String> validTypes = ImmutableSet.of("bool", "enum", "int", "string", "password", "path"); if (StringUtils.isBlank(type)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_CONFIG_DEF_MISSING_TYPE; failures.add(new ValidationFailureDetailsBuilder() .field("config def type") .subField(configName) .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(configName)) .build()); valid = false; } else if (!validTypes.contains(type)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_CONFIG_DEF_INVALID_TYPE; failures.add(new ValidationFailureDetailsBuilder() .field("config def type") .subField(configName) .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(type, configName, validTypes)) .build()); valid = false; } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidConfigType(%s, %s, %s): %s", type, configName, failures, valid)); } return valid; } boolean isValidResources(RangerServiceDef serviceDef, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidResources(%s, %s)", serviceDef, failures)); } boolean valid = true; List<RangerResourceDef> resources = serviceDef.getResources(); if (CollectionUtils.isEmpty(resources)) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_MISSING_FIELD; failures.add(new ValidationFailureDetailsBuilder() .field("resources") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage("resources")) .build()); valid = false; } else { Set<String> names = new HashSet<String>(resources.size()); Set<Long> ids = new HashSet<Long>(resources.size()); for (RangerResourceDef resource : resources) { /* * While id is the natural key, name is a surrogate key. At several places code expects resource name to be unique within a service. */ valid = isUnique(resource.getName(), names, "resource name", "resources", failures) && valid; valid = isUnique(resource.getItemId(), ids, "resource itemId", "resources", failures) && valid; } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidResources(%s, %s): %s", serviceDef, failures, valid)); } return valid; } boolean isValidResourceGraph(RangerServiceDef serviceDef, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidResourceGraph(%s, %s)", serviceDef, failures)); } boolean valid = true; // We don't want this helper to get into the cache or to use what is in the cache!! RangerServiceDefHelper defHelper = _factory.createServiceDefHelper(serviceDef, false); if (!defHelper.isResourceGraphValid()) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_RESOURCE_GRAPH_INVALID; failures.add(new ValidationFailureDetailsBuilder() .field("resource graph") .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage()) .build()); valid = false; } // resource level should be unique within a hierarchy for(int policyType : RangerPolicy.POLICY_TYPES) { Set<List<RangerResourceDef>> hierarchies = defHelper.getResourceHierarchies(policyType); for (List<RangerResourceDef> aHierarchy : hierarchies) { Set<Integer> levels = new HashSet<Integer>(aHierarchy.size()); for (RangerResourceDef resourceDef : aHierarchy) { valid = isUnique(resourceDef.getLevel(), levels, "resource level", "resources", failures) && valid; } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidResourceGraph(%s, %s): %s", serviceDef, failures, valid)); } return valid; } boolean isValidEnums(List<RangerEnumDef> enumDefs, List<ValidationFailureDetails> failures) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidEnums(%s, %s)", enumDefs, failures)); } boolean valid = true; if (CollectionUtils.isEmpty(enumDefs)) { LOG.debug("enum def collection passed in was null/empty. Ok."); } else { Set<String> names = new HashSet<>(); Set<Long> ids = new HashSet<>(); for (RangerEnumDef enumDef : enumDefs) { if (enumDef == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_ENUM_DEF_NULL_OBJECT; failures.add(new ValidationFailureDetailsBuilder() .field("enum def") .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage()) .build()); valid = false; } else { // enum-names and ids must non-blank and be unique to a service definition String enumName = enumDef.getName(); valid = isUnique(enumName, names, "enum def name", "enum defs", failures) && valid; valid = isUnique(enumDef.getItemId(), ids, "enum def itemId", "enum defs", failures) && valid; // enum must contain at least one valid value and those values should be non-blank and distinct if (CollectionUtils.isEmpty(enumDef.getElements())) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_ENUM_DEF_NO_VALUES; failures.add(new ValidationFailureDetailsBuilder() .field("enum values") .subField(enumName) .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(enumName)) .build()); valid = false; } else { valid = isValidEnumElements(enumDef.getElements(), failures, enumName) && valid; // default index should be valid int defaultIndex = getEnumDefaultIndex(enumDef); if (defaultIndex < 0 || defaultIndex >= enumDef.getElements().size()) { // max index is one less than the size of the elements list ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_ENUM_DEF_INVALID_DEFAULT_INDEX; failures.add(new ValidationFailureDetailsBuilder() .field("enum default index") .subField(enumName) .isSemanticallyIncorrect() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(defaultIndex, enumName)) .build()); valid = false; } } } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidEnums(%s, %s): %s", enumDefs, failures, valid)); } return valid; } boolean isValidEnumElements(List<RangerEnumElementDef> enumElementsDefs, List<ValidationFailureDetails> failures, String enumName) { if(LOG.isDebugEnabled()) { LOG.debug(String.format("==> RangerServiceDefValidator.isValidEnumElements(%s, %s)", enumElementsDefs, failures)); } boolean valid = true; if (CollectionUtils.isEmpty(enumElementsDefs)) { LOG.debug("Enum elements list passed in was null/empty!"); } else { // enum element names should be valid and distinct Set<String> elementNames = new HashSet<>(); Set<Long> ids = new HashSet<>(); for (RangerEnumElementDef elementDef : enumElementsDefs) { if (elementDef == null) { ValidationErrorCode error = ValidationErrorCode.SERVICE_DEF_VALIDATION_ERR_ENUM_DEF_NULL_ENUM_ELEMENT; failures.add(new ValidationFailureDetailsBuilder() .field("enum element") .subField(enumName) .isMissing() .errorCode(error.getErrorCode()) .becauseOf(error.getMessage(enumName)) .build()); valid = false; } else { valid = isUnique(elementDef.getName(), enumName, elementNames, "enum element name", "enum elements", failures) && valid; valid = isUnique(elementDef.getItemId(), enumName, ids, "enum element itemId", "enum elements", failures) && valid; } } } if(LOG.isDebugEnabled()) { LOG.debug(String.format("<== RangerServiceDefValidator.isValidEnumElements(%s, %s): %s", enumElementsDefs, failures, valid)); } return valid; } }