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

org.structr.common.SecurityContext Maven / Gradle / Ivy

Go to download

Structr is an open source framework based on the popular Neo4j graph database.

The newest version!
/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr .
 *
 * Structr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Structr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Structr.  If not, see .
 */
package org.structr.common;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.structr.core.GraphObject;
import org.structr.core.Services;
import org.structr.core.auth.Authenticator;
import org.structr.core.entity.AbstractNode;
import org.structr.core.entity.Principal;
import org.structr.core.entity.SuperUser;
import org.structr.core.graph.NodeInterface;
import org.structr.schema.SchemaHelper;

/**
 * Encapsulates the current user and access path and provides methods to query
 * permission flags for a given node. This is the place where HttpServletRequest
 * and Authenticator get together.
 *
 *
 */
public class SecurityContext {

	public static final String LOCALE_KEY = "locale";

	private static final Logger logger                   = Logger.getLogger(SecurityContext.class.getName());
	private static final Map resourceFlags = new ConcurrentHashMap<>();
	private static final Pattern customViewPattern       = Pattern.compile(".*properties=([a-zA-Z_,]+)");
	private boolean doTransactionNotifications           = true;
	private boolean dontModifyAccessTime                 = false;
	private boolean ignoreResultCount                    = false;

	//~--- fields ---------------------------------------------------------
	private final Map ranges = new ConcurrentHashMap<>();
	private final Map attrs      = new ConcurrentHashMap<>();
	private AccessMode accessMode                = AccessMode.Frontend;
	private Authenticator authenticator          = null;
	private Principal cachedUser                 = null;
	private HttpServletRequest request           = null;
	private HttpServletResponse response         = null;
	private Set customView               = null;
	private String cachedUserName                = null;
	private String cachedUserId                  = null;

	//~--- constructors ---------------------------------------------------
	private SecurityContext() {
	}

	/*
	 * Alternative constructor for stateful context, e.g. WebSocket
	 */
	private SecurityContext(Principal user, AccessMode accessMode) {

		this.cachedUser     = user;
		this.accessMode     = accessMode;
	}

	/*
	 * Alternative constructor for stateful context, e.g. WebSocket
	 */
	private SecurityContext(Principal user, HttpServletRequest request, AccessMode accessMode) {

		this(request);

		this.cachedUser = user;
		this.accessMode = accessMode;
	}

	private SecurityContext(HttpServletRequest request) {

		this.request = request;

		initializeCustomView(request);
		initializeQueryRanges(request);
		initializeHttpParameters(request);
	}

	private void initializeHttpParameters(final HttpServletRequest request) {

		if (request != null) {

			if ("disabled".equals(request.getHeader("Structr-Websocket-Broadcast"))) {
				this.doTransactionNotifications = false;
			}

			if (request.getParameter("ignoreResultCount") != null) {
				this.ignoreResultCount = true;
			}
		}
	}

	private void initializeCustomView(final HttpServletRequest request) {

		// check for custom view attributes
		if (request != null) {

			try {
				final String acceptedContentType = request.getHeader("Accept");
				if (acceptedContentType != null && acceptedContentType.startsWith("application/json")) {

					final Matcher matcher = customViewPattern.matcher(acceptedContentType);
					if (matcher.matches()) {

						customView = new LinkedHashSet<>();

						final String properties = matcher.group(1);
						final String[] parts = properties.split("[,]+");
						for (final String part : parts) {

							final String p = part.trim();
							if (p.length() > 0) {

								customView.add(p);
							}
						}
					}
				}

			} catch (Throwable ignore) {
			}
		}

	}

	private void initializeQueryRanges(final HttpServletRequest request) {

		if (request != null) {

			final String rangeSource = request.getHeader("Range");
			if (rangeSource != null) {

				final String[] rangeParts = rangeSource.split("[;]+");
				final int rangeCount      = rangeParts.length;

				for (int i = 0; i < rangeCount; i++) {

					final String[] parts = rangeParts[i].split("[=]+");
					if (parts.length == 2) {

						final String identifier = parts[0].trim();
						final String valueRange = parts[1].trim();

						if (StringUtils.isNotBlank(identifier) && StringUtils.isNotBlank(valueRange)) {

							if (valueRange.contains(",")) {

								logger.log(Level.WARNING, "Unsupported Range header specification {0}, multiple ranges are not supported.", valueRange);

							} else {

								final String[] valueParts = valueRange.split("[-]+");
								if (valueParts.length == 2) {

									String startString = valueParts[0].trim();
									String endString = valueParts[1].trim();

									// remove optional total size indicator
									if (endString.contains("/")) {
										endString = endString.substring(0, endString.indexOf("/"));
									}

									try {

										final int start    = Integer.parseInt(startString);
										final int end      = Integer.parseInt(endString);

										ranges.put(identifier, new QueryRange(start, end));

									} catch (Throwable t) {

										logger.log(Level.WARNING, "", t);
									}
								}
							}

						}
					}
				}
			}
		}
	}

	public static void clearResourceFlag(final String resource, long flag) {

		final String name     = SchemaHelper.normalizeEntityName(resource);
		final Long flagObject = resourceFlags.get(name);
		long flags            = 0;

		if (flagObject != null) {

			flags = flagObject;
		}

		flags &= ~flag;

		resourceFlags.put(name, flags);

	}

	public void removeForbiddenNodes(List nodes, final boolean includeDeletedAndHidden, final boolean publicOnly) {

		boolean readableByUser = false;

		for (Iterator it = nodes.iterator(); it.hasNext();) {

			GraphObject obj = it.next();

			if (obj instanceof AbstractNode) {

				AbstractNode n = (AbstractNode) obj;

				readableByUser = n.isGranted(Permission.read, this);

				if (!(readableByUser && (includeDeletedAndHidden || !n.isDeleted()) && (n.isVisibleToPublicUsers() || !publicOnly))) {

					it.remove();
				}

			}

		}

	}

	public static SecurityContext getSuperUserInstance(HttpServletRequest request) {
		return new SuperUserSecurityContext(request);
	}

	public static SecurityContext getSuperUserInstance() {
		return new SuperUserSecurityContext();

	}

	public static SecurityContext getInstance(Principal user, AccessMode accessMode) {
		return new SecurityContext(user, accessMode);

	}

	public static SecurityContext getInstance(Principal user, HttpServletRequest request, AccessMode accessMode) {
		return new SecurityContext(user, request, accessMode);

	}

	public HttpSession getSession() {

		if (request != null) {

			final HttpSession session = request.getSession(false);
			if (session != null) {

				session.setMaxInactiveInterval(Services.getGlobalSessionTimeout());
			}

			return session;

		}

		return null;

	}

	public HttpServletRequest getRequest() {

		return request;

	}

	public HttpServletResponse getResponse() {

		return response;

	}

	public String getCachedUserId() {
		return cachedUserId;
	}

	public String getCachedUserName() {
		return cachedUserName;
	}

	public Principal getCachedUser() {
		return cachedUser;
	}

	public Principal getUser(final boolean tryLogin) {

		// If we've got a user, return it! Easiest and fastest!!
		if (cachedUser != null) {

			// update caches, we can safely assume a transaction context here
			if (cachedUserId == null) {
				this.cachedUserId   = cachedUser.getUuid();
			}

			// update caches, we can safely assume a transaction context here
			if (cachedUserName == null) {
				this.cachedUserName = cachedUser.getName();
			}

			return cachedUser;

		}

		if (authenticator == null) {

			return null;

		}

		if (authenticator.hasExaminedRequest()) {

			// If the authenticator has already examined the request,
			// we assume that we will not get new information.
			// Otherwise, the cachedUser would have been != null
			// and we would not land here.
			return null;

		}

		try {

			cachedUser = authenticator.getUser(request, tryLogin);
			if (cachedUser != null) {

				cachedUserId   = cachedUser.getUuid();
				cachedUserName = cachedUser.getName();
			}


		} catch (Throwable t) {

			logger.log(Level.WARNING, "No user found");

		}

		return cachedUser;

	}

	public AccessMode getAccessMode() {

		return accessMode;

	}

	public boolean hasParameter(final String name) {
		return request != null && request.getParameter(name) != null;
	}

	public StringBuilder getBaseURI() {

		final StringBuilder uriBuilder = new StringBuilder(200);

		uriBuilder.append(request.getScheme());
		uriBuilder.append("://");
		uriBuilder.append(request.getServerName());
		uriBuilder.append(":");
		uriBuilder.append(request.getServerPort());
		uriBuilder.append(request.getContextPath());
		uriBuilder.append(request.getServletPath());
		uriBuilder.append("/");

		return uriBuilder;

	}

	public Object getAttribute(String key) {

		return attrs.get(key);

	}

	public static long getResourceFlags(String resource) {

		final String name     = SchemaHelper.normalizeEntityName(resource);
		final Long flagObject = resourceFlags.get(name);
		long flags            = 0;

		if (flagObject != null) {

			flags = flagObject;
		} else {

			logger.log(Level.FINE, "No resource flag set for {0}", resource);
		}

		return flags;

	}

	public static boolean hasFlag(String resourceSignature, long flag) {

		return (getResourceFlags(resourceSignature) & flag) == flag;

	}

	public boolean isSuperUser() {

		Principal user = getUser(false);

		return ((user != null) && (user instanceof SuperUser || user.getProperty(Principal.isAdmin)));

	}

	public boolean isVisible(AccessControllable node) {

		switch (accessMode) {

			case Backend:
				return isVisibleInBackend(node);

			case Frontend:
				return isVisibleInFrontend(node);

			default:
				return false;

		}

	}

	public boolean isReadable(final NodeInterface node, final boolean includeDeletedAndHidden, final boolean publicOnly) {

		/**
		 * The if-clauses in the following lines have been split for
		 * performance reasons.
		 */
		// deleted and hidden nodes will only be returned if we are told to do so
		if ((node.isDeleted() || node.isHidden()) && !includeDeletedAndHidden) {

			return false;
		}

		// visibleToPublic overrides anything else
		// Publicly visible nodes will always be returned
		if (node.isVisibleToPublicUsers()) {

			return true;
		}

		// Next check is only for non-public nodes, because
		// public nodes are already added one step above.
		if (publicOnly) {

			return false;
		}

		// Ask for user only if node is visible for authenticated users
		if (node.isVisibleToAuthenticatedUsers() && getUser(false) != null) {

			return true;
		}

		return node.isGranted(Permission.read, this);
	}

	// ----- private methods -----
	private boolean isVisibleInBackend(AccessControllable node) {

		if (isVisibleInFrontend(node)) {

			return true;

		}

		// no node, nothing to see here..
		if (node == null) {

			return false;
		}

		// fetch user
		final Principal user = getUser(false);

		// anonymous users may not see any nodes in backend
		if (user == null) {

			return false;
		}

		// SuperUser may always see the node
		if (user instanceof SuperUser) {

			return true;
		}

		return node.isGranted(Permission.read, this);
	}

	/**
	 * Indicates whether the given node is visible for a frontend request.
	 * This method should be used to explicetely check visibility of the
	 * requested root element, like e.g. a page, a partial or a file/image
	 * to download.
	 *
	 * It should *not* be used to check accessibility of child nodes because
	 * it might send a 401 along with a request for basic authentication.
	 *
	 * For those, use
	 * {@link SecurityContext#isReadable(org.structr.core.entity.AbstractNode, boolean, boolean)}
	 *
	 * @param node
	 * @return isVisible
	 */
	private boolean isVisibleInFrontend(AccessControllable node) {

		if (node == null) {

			return false;
		}

		// check hidden flag
		if (node.isHidden()) {

			return false;
		}

		// Fetch already logged-in user, if present (don't try to login)
		final Principal user = getUser(false);

		if (user != null) {

			final Principal owner = node.getOwnerNode();

			// owner is always allowed to do anything with its nodes
			if (user.equals(node) || user.equals(owner) || user.getParents().contains(owner)) {

				return true;
			}

		}

		// Public nodes are visible to non-auth users only
		if (node.isVisibleToPublicUsers() && user == null) {

			return true;
		}

		// Ask for user only if node is visible for authenticated users
		if (node.isVisibleToAuthenticatedUsers()) {

			if (user != null) {

				return true;
			}
		}

		return node.isGranted(Permission.read, this);

	}

	public void setRequest(HttpServletRequest request) {
		this.request = request;
	}

	public void setResponse(HttpServletResponse response) {
		this.response = response;
	}

	public static void setResourceFlag(final String resource, long flag) {

		final String name     = SchemaHelper.normalizeEntityName(resource);
		final Long flagObject = resourceFlags.get(name);
		long flags            = 0;

		if (flagObject != null) {

			flags = flagObject;
		}

		flags |= flag;

		resourceFlags.put(name, flags);

	}

	public void setAttribute(String key, Object value) {

		attrs.put(key, value);

	}

	public void setAccessMode(AccessMode accessMode) {

		this.accessMode = accessMode;

	}

	public void clearCustomView() {
		customView = new LinkedHashSet<>();
	}

	public void setCustomView(final String... properties) {

		customView = new LinkedHashSet<>();

		for (final String prop : properties) {
			customView.add(prop);
		}

	}

	public Authenticator getAuthenticator() {
		return authenticator;
	}

	public void setAuthenticator(final Authenticator authenticator) {
		this.authenticator = authenticator;
	}

	public boolean hasCustomView() {
		return customView != null && !customView.isEmpty();
	}

	public Set getCustomView() {
		return customView;
	}

	public QueryRange getRange(final String key) {
		return ranges.get(key);
	}

	/**
	 * Determine the effective locale for this request.
	 *
	 * Priority 1: URL parameter "locale" Priority 2: User locale  3: Browser locale  4: Default locale
	 *
	 * @return locale
	 */
	public Locale getEffectiveLocale() {

		Locale locale = Locale.getDefault();
		boolean userHasLocaleString = false;

		if (cachedUser != null) {

			final String userLocaleString = cachedUser.getProperty(Principal.locale);

			if (userLocaleString != null) {
				userHasLocaleString = true;

				try {
					locale = LocaleUtils.toLocale(userLocaleString);
				} catch (IllegalArgumentException e) {
					locale = Locale.forLanguageTag(userLocaleString);
				}
			}

		}

		if (request != null) {

			if (!userHasLocaleString) {
				locale = request.getLocale();
			}

			// Overwrite locale if requested by URL parameter
			String requestedLocaleString = request.getParameter(LOCALE_KEY);
			if (StringUtils.isNotBlank(requestedLocaleString)) {
				try {
					locale = LocaleUtils.toLocale(requestedLocaleString);
				} catch (IllegalArgumentException e) {
					locale = Locale.forLanguageTag(requestedLocaleString);
				}
			}

		}

		return locale;
	}

	public String getCompoundRequestURI() {

		if (request != null) {

			if (request.getQueryString() != null) {

				return request.getRequestURI().concat("?").concat(request.getQueryString());

			} else {

				return request.getRequestURI();

			}

		}

		return "[No request available]";
	}

	public boolean isDoTransactionNotifications() {
		return doTransactionNotifications;
	}

	public void setDoTransactionNotifications(boolean doTransactionNotifications) {
		this.doTransactionNotifications = doTransactionNotifications;
	}

	public boolean dontModifyAccessTime() {
		return dontModifyAccessTime;
	}

	public void preventModificationOfAccessTime() {
		dontModifyAccessTime = true;
	}

	public void ignoreResultCount(final boolean doIgnore) {
		this.ignoreResultCount = doIgnore;
	}

	public boolean ignoreResultCount() {
		return ignoreResultCount;
	}

	// ----- nested classes -----
	private static class SuperUserSecurityContext extends SecurityContext {

		private static final SuperUser superUser = new SuperUser();

		public SuperUserSecurityContext(HttpServletRequest request) {
			super(request);
		}

		public SuperUserSecurityContext() {
		}

		//~--- get methods --------------------------------------------

		@Override
		public Principal getUser(final boolean tryLogin) {

			return new SuperUser();

		}

		@Override
		public Principal getCachedUser() {
			return superUser;
		}

		@Override
		public String getCachedUserId() {
			return Principal.SUPERUSER_ID;
		}

		@Override
		public String getCachedUserName() {
			return "superadmin";
		}

		@Override
		public AccessMode getAccessMode() {

			return (AccessMode.Backend);

		}

		@Override
		public boolean isReadable(final NodeInterface node, final boolean includeDeletedAndHidden, final boolean publicOnly) {

			return true;
		}

		@Override
		public boolean isVisible(AccessControllable node) {

			return true;

		}

		@Override
		public boolean isSuperUser() {

			return true;

		}

	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy