All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.vertigo.ui.core.ViewContextUpdateSecurity Maven / Gradle / Ivy

/**
 * vertigo - application development platform
 *
 * Copyright (C) 2013-2022, Vertigo.io, [email protected]
 *
 * Licensed 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 io.vertigo.ui.core;

import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import io.vertigo.account.authorization.VSecurityException;
import io.vertigo.core.locale.MessageText;

/**
 * Keep the viewContext updatable data allowed.
 * A state securedUpdates, defined when this security is enabled (controllers could always update data).
 * @author npiedeloup
 */
public final class ViewContextUpdateSecurity implements Serializable {

	private static final int SPLIT_OBJECT_INDEX = 0;
	private static final int SPLIT_ROW_INDEX = 1;
	private static final int SPLIT_FIELD_INDEX = 2;

	private static final String ALL_LINES_PARAM_NAME = "[*]";
	private static final long serialVersionUID = -4185584640736172927L;
	private static final String FORBIDDEN_DATA_UPDATE_MESSAGE = "These data/field can't be accepted ({0})"; //no too sharp info here : may use log //TODO externalized msg

	private static final ViewContextUpdateSecurity UNMODIFIABLE_INSTANCE = new ViewContextUpdateSecurity();
	static {
		UNMODIFIABLE_INSTANCE.setCheckUpdates(true);
	}

	public static ViewContextUpdateSecurity unmodifiable() {
		return UNMODIFIABLE_INSTANCE;
	}

	private Boolean checkUpdates = Boolean.FALSE;
	private final Map> updatablesKeys = new HashMap<>();
	private final Map> updatablesRows = new HashMap<>();
	private final Set allowedFields = new HashSet<>();

	public void assertIsUpdatable(final String object, final String fieldName) {
		final String[] splitObject = splitObjectName(object);
		final String objectKey = splitObject[SPLIT_OBJECT_INDEX];
		final String row = splitObject[SPLIT_ROW_INDEX];

		if (checkUpdates && !isAllowedField(objectKey, row, fieldName)) {
			throw new VSecurityException(MessageText.of(FORBIDDEN_DATA_UPDATE_MESSAGE, object + "." + fieldName));
		}
	}

	public void assertIsUpdatable(final String object) {
		if (checkUpdates && !updatablesKeys.containsKey(object)) {
			throw new VSecurityException(MessageText.of(FORBIDDEN_DATA_UPDATE_MESSAGE, object));
		}
	}

	/**
	 * Fixe le mode d'update : filtré ou non (par les champs éditables de l'ihm).
	 */
	public void setCheckUpdates(final boolean checkUpdates) {
		this.checkUpdates = checkUpdates;
	}

	public void addUpdatableKey(final String object, final String fieldName, final String rowIndex) {
		if (rowIndex == null) {
			updatablesKeys.computeIfAbsent(object, k -> new HashSet<>()).add(fieldName);
			allowedFields.add("vContext[" + object + "][" + fieldName + "]");
		} else if (isNumeric(rowIndex)) {
			//final String objectKey = object + "[" + rowIndex + "]";
			updatablesKeys.computeIfAbsent(object, k -> new HashSet<>()).add(fieldName);
			updatablesRows.computeIfAbsent(object, k -> new HashSet<>()).add(rowIndex);
			allowedFields.add("vContext[" + object + "][" + rowIndex + "][" + fieldName + "]");
		} else {
			updatablesKeys.computeIfAbsent(object, k -> new HashSet<>()).add(fieldName);
			updatablesRows.computeIfAbsent(object, k -> new HashSet<>()).add("*");
			allowedFields.add("vContext[" + object + "]" + ALL_LINES_PARAM_NAME + "[" + fieldName + "]");
		}
	}

	private static boolean isNumeric(final String cs) {
		//more efficient than regexp; needed because it can't be hugely used (many request parameter)
		if (cs == null || cs.isBlank()) {
			return false;
		}
		final int sz = cs.length();
		for (int i = 0; i < sz; i++) {
			if (!Character.isDigit(cs.charAt(i))) {
				return false;
			}
		}
		return true;
	}

	public void addUpdatableKey(final String object) {
		updatablesKeys.computeIfAbsent(object, k -> Collections.emptySet());
		allowedFields.add("vContext[" + object + "]");
	}

	public void assertAllowedFields(final Enumeration parameterNames) {
		final Optional firstDisallowedField = Collections.list(parameterNames).stream()
				.filter(n -> n.startsWith("vContext["))
				.filter(n -> !isAllowedField(n))
				.findFirst(); //faster than findFirst
		if (firstDisallowedField.isPresent()) {
			throw new VSecurityException(MessageText.of(FORBIDDEN_DATA_UPDATE_MESSAGE, firstDisallowedField.orElse("<>")));
		}
	}

	private boolean isAllowedField(final String paramName) {
		if (allowedFields.contains(paramName)) {
			return true;
		}
		if (isPrimitiveParam(paramName)) {
			// primitive data must be in allowedFields
			return false;
		}
		final String[] splitParamName = splitParamName(paramName);
		final String object = splitParamName[SPLIT_OBJECT_INDEX];
		final String row = splitParamName[SPLIT_ROW_INDEX];
		final String fieldName = splitParamName[SPLIT_FIELD_INDEX];
		return isAllowedField(object, row, fieldName);
	}

	private static boolean isPrimitiveParam(final String paramName) {
		final int firstParam = paramName.indexOf('[');
		return paramName.indexOf('[', firstParam + 1) == -1;
	}

	private static String[] splitObjectName(final String objectName) {
		final String[] result = new String[3];
		final int rowIndex = objectName.indexOf('[');
		final int rowEndIndex = objectName.indexOf(']', rowIndex);
		if (rowIndex > 0) {
			result[SPLIT_OBJECT_INDEX] = objectName.substring(0, rowIndex);
			result[SPLIT_ROW_INDEX] = objectName.substring(rowIndex + 1, rowEndIndex);

		} else {
			result[SPLIT_OBJECT_INDEX] = objectName;
		}

		return result;
	}

	private static String[] splitParamName(final String paramName) {
		final String[] result = new String[3];
		final int objectIndex = paramName.indexOf('[');
		final int objectEndIndex = paramName.indexOf(']', objectIndex);
		result[SPLIT_OBJECT_INDEX] = paramName.substring(objectIndex + 1, objectEndIndex);

		final int rowFieldIndex1 = paramName.indexOf('[', objectIndex + 1);
		final int rowFieldEndIndex1 = paramName.indexOf(']', objectEndIndex + 1);
		final int rowFieldIndex2 = paramName.indexOf('[', rowFieldIndex1 + 1);
		final int rowFieldEndIndex2 = paramName.indexOf(']', rowFieldEndIndex1 + 1);
		if (rowFieldIndex2 == -1) {
			result[SPLIT_FIELD_INDEX] = paramName.substring(rowFieldIndex1 + 1, rowFieldEndIndex1);
		} else {
			result[SPLIT_ROW_INDEX] = paramName.substring(rowFieldIndex1 + 1, rowFieldEndIndex1);
			result[SPLIT_FIELD_INDEX] = paramName.substring(rowFieldIndex2 + 1, rowFieldEndIndex2);
		}
		return result;
	}

	private boolean isAllowedField(final String object, final String row, final String fieldName) {
		//le champ doit être modifiable
		final Set updatablesObjectsKeys = updatablesKeys.get(object);
		final Set updatablesObjectsRows = updatablesRows.get(object);

		if (updatablesObjectsKeys != null && updatablesObjectsKeys.contains(fieldName)
		//Et soit la ligne est null
				&& (row == null ||
				//soit elle est modifibable ou toute les lignes sont modifiables
						updatablesObjectsRows != null
								&& (updatablesObjectsRows.contains(row) || updatablesObjectsRows.contains("*")))) {
			return true;
		}
		return false;
	}

	public String[] getAllowedFields() {
		return allowedFields.toArray(s -> new String[s]);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy