001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.auth.roles.common;
017
018import java.security.Principal;
019import java.util.Arrays;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import javax.jcr.Node;
026import javax.jcr.NodeIterator;
027import javax.jcr.RepositoryException;
028import javax.jcr.Session;
029
030import org.fcrepo.auth.common.FedoraAuthorizationDelegate;
031import org.fcrepo.http.commons.session.SessionFactory;
032import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
033import org.modeshape.jcr.value.Path;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036import org.springframework.beans.factory.annotation.Autowired;
037
038/**
039 * Policy enforcement point for roles-based authentication
040 * @author Gregory Jansen
041 */
042public abstract class AbstractRolesAuthorizationDelegate implements FedoraAuthorizationDelegate {
043
044    private static final Logger LOGGER = LoggerFactory
045            .getLogger(AbstractRolesAuthorizationDelegate.class);
046
047    protected static final String AUTHZ_DETECTION = "/{" +
048            Constants.JcrName.NS_URI + "}";
049
050    private static final String[] REMOVE_ACTIONS = {"remove"};
051
052    @Autowired
053    private AccessRolesProvider accessRolesProvider = null;
054
055    @Autowired
056    private SessionFactory sessionFactory = null;
057
058    /**
059     * Gather effectives roles
060     *
061     * @param acl access control list
062     * @param principals effective principals
063     * @return set of effective content roles
064     */
065    public static Set<String>
066    resolveUserRoles(final Map<String, List<String>> acl,
067                    final Set<Principal> principals) {
068        final Set<String> roles = new HashSet<>();
069        for (final Principal p : principals) {
070            final List<String> matchedRoles = acl.get(p.getName());
071            if (matchedRoles != null) {
072                LOGGER.debug("request principal matched role assignment: {}", p.getName());
073                roles.addAll(matchedRoles);
074            }
075        }
076        return roles;
077    }
078
079    @Override
080    public boolean hasPermission(final Session session, final Path absPath, final String[] actions) {
081        LOGGER.debug("Does user have permission for actions: {}, on path: {}", actions, absPath);
082        final boolean permission = doHasPermission(session, absPath, actions);
083
084        LOGGER.debug("Permission for actions: {}, on: {} = {}", actions, absPath, permission);
085        return permission;
086    }
087
088    private boolean doHasPermission(final Session session, final Path absPath, final String[] actions) {
089        final Set<String> roles;
090
091        final Principal userPrincipal = getUserPrincipal(session);
092        if (userPrincipal == null) {
093            return false;
094        }
095
096        final Set<Principal> allPrincipals = getPrincipals(session);
097        if (allPrincipals == null) {
098            return false;
099        }
100
101        try {
102            final Session internalSession = sessionFactory.getInternalSession();
103            final Map<String, List<String>> acl =
104                    accessRolesProvider.findRolesForPath(absPath,
105                            internalSession);
106            roles = resolveUserRoles(acl, allPrincipals);
107            LOGGER.debug("roles for this request: {}", roles);
108        } catch (final RepositoryException e) {
109            throw new RepositoryRuntimeException("Cannot look up node information on " + absPath +
110                    " for permissions check.", e);
111        }
112
113        if (LOGGER.isDebugEnabled()) {
114            LOGGER.debug("roles: {}, actions: {}, path: {}", roles, actions, absPath);
115            if (actions.length > 1) { // have yet to see more than one
116                LOGGER.debug("FOUND MULTIPLE ACTIONS: {}", Arrays
117                        .toString(actions));
118            }
119        }
120
121        if (actions.length == 1 && "remove_child_nodes".equals(actions[0])) {
122            // in roles-based ACLs, the permission to remove children is
123            // conferred by earlier check for "remove_node" on the child node
124            // itself.
125            return true;
126        }
127
128        if (!rolesHavePermission(session, absPath.toString(), actions, roles)) {
129            return false;
130        }
131
132        if (actions.length == 1 && "remove".equals(actions[0])) {
133            // you must be able to delete all the children
134            // TODO make recursive/ACL-query-based check configurable
135            return canRemoveChildrenRecursive(session, absPath.toString(),
136                    allPrincipals, roles);
137        }
138        return true;
139    }
140
141    private static Principal getUserPrincipal(final Session session) {
142        final Object value = session.getAttribute(FEDORA_USER_PRINCIPAL);
143        if (value instanceof Principal) {
144            return (Principal) value;
145        }
146        return null;
147    }
148
149    @SuppressWarnings("unchecked")
150    private static Set<Principal> getPrincipals(final Session session) {
151        final Object value = session.getAttribute(FEDORA_ALL_PRINCIPALS);
152        if (value instanceof Set<?>) {
153            return (Set<Principal>) value;
154        }
155        return null;
156    }
157
158    /**
159     * @param userSession the user session
160     * @param parentPath the parent path
161     * @param allPrincipals all principals
162     * @param parentRoles the roles on the parent
163     * @return true if permitted
164     */
165    private boolean canRemoveChildrenRecursive(final Session userSession,
166                                               final String parentPath,
167                                               final Set<Principal> allPrincipals,
168                                               final Set<String> parentRoles) {
169        try {
170            final Session internalSession = sessionFactory.getInternalSession();
171            LOGGER.debug("Recursive child remove permission checks for: {}",
172                         parentPath);
173            final Node parent = internalSession.getNode(parentPath);
174            if (!parent.hasNodes()) {
175                return true;
176            }
177            final NodeIterator ni = parent.getNodes();
178            while (ni.hasNext()) {
179                final Node n = ni.nextNode();
180                // are there unique roles?
181                final Set<String> roles;
182                final Map<String, List<String>> acl = accessRolesProvider.getRoles(n, false);
183
184                if (acl != null) {
185                    roles = resolveUserRoles(acl, allPrincipals);
186                } else {
187                    roles = parentRoles;
188                }
189                if (rolesHavePermission(userSession, n.getPath(),
190                        REMOVE_ACTIONS,
191                        roles)) {
192
193                    if (!canRemoveChildrenRecursive(userSession, n.getPath(),
194                            allPrincipals, roles)) {
195                        return false;
196                    }
197                } else {
198                    LOGGER.info("Remove permission denied at {} with roles {}", n.getPath(), roles);
199                    return false;
200                }
201            }
202            return true;
203        } catch (final RepositoryException e) {
204            throw new RepositoryRuntimeException(
205                    "Cannot lookup child permission check information for " +
206                            parentPath, e);
207        }
208    }
209
210    /**
211     * Subclasses must override this method to determine permissions based on
212     * supplied roles.
213     * 
214     * @param userSession the user session
215     * @param absPath path to the object
216     * @param actions requested action
217     * @param roles effective roles for this request and content
218     * @return true if role has permission
219     */
220    public abstract boolean rolesHavePermission(final Session userSession, final String absPath,
221            final String[] actions, final Set<String> roles);
222
223}