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.util.ArrayList;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023
024import javax.jcr.ItemNotFoundException;
025import javax.jcr.Node;
026import javax.jcr.NodeIterator;
027import javax.jcr.PathNotFoundException;
028import javax.jcr.RepositoryException;
029import javax.jcr.Session;
030import javax.jcr.Value;
031
032import org.fcrepo.auth.roles.common.Constants.JcrName;
033import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
034import org.modeshape.jcr.value.Path;
035import org.slf4j.Logger;
036import org.springframework.stereotype.Component;
037
038import static com.google.common.collect.Iterables.toArray;
039import static java.util.Collections.emptyMap;
040import static org.fcrepo.auth.roles.common.Constants.registerPrefixes;
041import static org.fcrepo.auth.roles.common.Constants.JcrName.Assignment;
042import static org.fcrepo.auth.roles.common.Constants.JcrName.Rbacl;
043import static org.fcrepo.auth.roles.common.Constants.JcrName.assignment;
044import static org.fcrepo.auth.roles.common.Constants.JcrName.principal;
045import static org.fcrepo.auth.roles.common.Constants.JcrName.rbacl;
046import static org.fcrepo.auth.roles.common.Constants.JcrName.rbaclAssignable;
047import static org.fcrepo.auth.roles.common.Constants.JcrName.role;
048import static org.slf4j.LoggerFactory.getLogger;
049
050/**
051 * Provides the effective access roles for authorization.
052 *
053 * @author Gregory Jansen
054 */
055@Component
056public class AccessRolesProvider {
057
058    private static final Logger LOGGER = getLogger(AccessRolesProvider.class);
059
060    public static final Map<String, List<String>> DEFAULT_ACCESS_ROLES = emptyMap();
061
062    /**
063     * Get the roles assigned to this Node. Optionally search up the tree for
064     * the effective roles.
065     *
066     * @param node the subject Node
067     * @param effective if true then search for effective roles
068     * @return a set of roles for each principal
069     */
070    public Map<String, List<String>> getRoles(final Node node, final boolean effective) {
071        try {
072            LOGGER.debug("Finding roles for: {}, effective={}", node.getPath(), effective);
073        } catch (RepositoryException e) {
074            LOGGER.debug("Unable to get path! {}", e.getMessage());
075        }
076
077        final Map<String, List<String>> data = new HashMap<>();
078        try {
079
080            final Session session = node.getSession();
081            registerPrefixes(session);
082            if (node.isNodeType(rbaclAssignable.getQualified())) {
083                getAssignments(node, data);
084                return data;
085            }
086            if (effective) { // look up the tree
087                try {
088                    for (Node n = node.getParent(); n != null; n = n.getParent()) {
089                        if (n.isNodeType(rbaclAssignable.getQualified())) {
090                            if (LOGGER.isDebugEnabled()) {
091                                LOGGER.debug("effective roles are assigned at node: {}", n.getPath());
092                            }
093                            getAssignments(n, data);
094                            if (LOGGER.isDebugEnabled()) {
095                                for (final Map.Entry<String, List<String>> entry : data.entrySet()) {
096                                    LOGGER.debug("{} has role(s) {}", entry.getKey(), entry.getValue());
097                                }
098                            }
099                            return data;
100                        }
101                    }
102                } catch (final ItemNotFoundException e) {
103                    LOGGER.debug("Subject not found, using default access roles: {}", e.getMessage());
104                    return DEFAULT_ACCESS_ROLES;
105                }
106            }
107        } catch (final RepositoryException e) {
108            throw new RepositoryRuntimeException(e);
109        }
110        return null;
111    }
112
113    /**
114     * @param node
115     * @param data
116     * @throws RepositoryException
117     */
118    private void getAssignments(final Node node, final Map<String, List<String>> data)
119        throws RepositoryException {
120
121        if (node.isNodeType(rbaclAssignable.getQualified())) {
122            try {
123                final Node rbacl = node.getNode(JcrName.rbacl.getQualified());
124                LOGGER.debug("got rbacl: {}", rbacl);
125                for (final NodeIterator ni = rbacl.getNodes(); ni.hasNext();) {
126                    final Node assign = ni.nextNode();
127                    final String principalName =
128                            assign.getProperty(principal.getQualified())
129                                    .getString();
130                    if (principalName == null ||
131                            principalName.trim().length() == 0) {
132                        LOGGER.warn("found empty principal name on node {}",
133                                    node.getPath());
134                    } else {
135                        List<String> roles = data.get(principalName);
136                        if (roles == null) {
137                            roles = new ArrayList<>();
138                            data.put(principalName, roles);
139                        }
140                        for (final Value v : assign.getProperty(
141                                role.getQualified()).getValues()) {
142                            if (v == null || v.toString().trim().length() == 0) {
143                                LOGGER.warn("found empty role name on node {}",
144                                            node.getPath());
145                            } else {
146                                roles.add(v.toString());
147                            }
148                        }
149                    }
150                }
151            } catch (final PathNotFoundException e) {
152                LOGGER.info(
153                             "Found rbaclAssignable mixin without a corresponding node at {}",
154                             node.getPath());
155            }
156        }
157    }
158
159    /**
160     * Assigns the given set of roles to each principal.
161     *
162     * @param node the Node to edit
163     * @param data the roles to assign
164     * @throws RepositoryException if repository exception occurred
165     */
166    public void postRoles(final Node node, final Map<String, Set<String>> data)
167        throws RepositoryException {
168        final Session session = node.getSession();
169        registerPrefixes(session);
170        if (!node.isNodeType(rbaclAssignable.getQualified())) {
171            node.addMixin(rbaclAssignable.getQualified());
172            LOGGER.debug("added rbaclAssignable type");
173        }
174
175        Node acl;
176
177        if (node.hasNode(rbacl.getQualified())) {
178            acl = node.getNode(rbacl.getQualified());
179            for (final NodeIterator ni = acl.getNodes(); ni.hasNext();) {
180                ni.nextNode().remove();
181            }
182        } else {
183            acl = node.addNode(rbacl.getQualified(), Rbacl.getQualified());
184        }
185
186        for (final Map.Entry<String, Set<String>> entry : data.entrySet()) {
187            final Node assign = acl.addNode(assignment.getQualified(), Assignment.getQualified());
188            assign.setProperty(principal.getQualified(), entry.getKey());
189            assign.setProperty(role.getQualified(), toArray(entry.getValue(), String.class));
190        }
191    }
192
193    /**
194     * Deletes all roles assigned on this node and removes the mixin type.
195     *
196     * @param node the node to delete
197     * @throws RepositoryException if delete failed
198     */
199    public void deleteRoles(final Node node) throws RepositoryException {
200        final Session session = node.getSession();
201        registerPrefixes(session);
202        if (node.isNodeType(rbaclAssignable.getQualified())) {
203            // remove rbacl child
204            try {
205                final Node rbacl = node.getNode(JcrName.rbacl.getQualified());
206                rbacl.remove();
207            } catch (final PathNotFoundException e) {
208                LOGGER.debug("Cannot find node: {}", node, e);
209            }
210            // remove mixin
211            node.removeMixin(rbaclAssignable.getQualified());
212        }
213    }
214
215    /**
216     * Finds effective roles assigned to a path, using first real ancestor node.
217     *
218     * @param absPath the real or potential node path
219     * @param session session
220     * @return the roles assigned to each principal
221     * @throws RepositoryException if PathNotFoundException can not handle
222     */
223    public Map<String, List<String>> findRolesForPath(final Path absPath,
224            final Session session) throws RepositoryException {
225        Node node = null;
226        for (Path p = absPath; p != null; p = p.getParent()) {
227            try {
228                if (p.isRoot()) {
229                    node = session.getRootNode();
230                } else {
231                    node = session.getNode(p.getString());
232                }
233                break;
234            } catch (final PathNotFoundException e) {
235                LOGGER.trace("Cannot find node: {}, trying parent.", p, e);
236            }
237        }
238        return this.getRoles(node, true);
239    }
240
241}