org.structr.common.SecurityContext.java Source code

Java tutorial

Introduction

Here is the source code for org.structr.common.SecurityContext.java

Source

/**
 * Copyright (C) 2010-2016 Structr GmbH
 *
 * This file is part of Structr <http://structr.org>.
 *
 * 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 <http://www.gnu.org/licenses/>.
 */
package org.structr.common;

import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
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.neo4j.helpers.collection.LruMap;
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<String, Long> resourceFlags = new LruMap<>(10000);
    private static final Pattern customViewPattern = Pattern.compile(".*properties=([a-zA-Z_,]+)");
    private boolean doTransactionNotifications = true;
    private boolean dontModifyAccessTime = false;

    //~--- fields ---------------------------------------------------------
    private final Map<String, QueryRange> ranges = new ConcurrentHashMap<>();
    private final Map<String, Object> attrs = Collections.synchronizedMap(new LinkedHashMap<String, Object>());
    private AccessMode accessMode = AccessMode.Frontend;
    private Authenticator authenticator = null;
    private Principal cachedUser = null;
    private HttpServletRequest request = null;
    private HttpServletResponse response = null;
    private Set<String> 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) {

            try {

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

            } catch (Throwable t) {
            }

        }
    }

    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) {

                                        t.printStackTrace();
                                    }
                                }
                            }

                        }
                    }
                }
            }
        }
    }

    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<? extends GraphObject> nodes, final boolean includeDeletedAndHidden,
            final boolean publicOnly) {

        boolean readableByUser = false;

        for (Iterator<? extends GraphObject> 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<String> 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 boolean isDoTransactionNotifications() {
        return doTransactionNotifications;
    }

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

    public boolean dontModifyAccessTime() {
        return dontModifyAccessTime;
    }

    public void preventModificationOfAccessTime() {
        dontModifyAccessTime = true;
    }

    // ----- 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;

        }

    }

}