package com.thinkbiganalytics.metadata.modeshape.security.action; import com.thinkbiganalytics.metadata.api.MetadataAccess; /*- * #%L * thinkbig-metadata-modeshape * %% * Copyright (C) 2017 ThinkBig Analytics * %% * 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. * #L% */ import com.thinkbiganalytics.metadata.api.MetadataException; import com.thinkbiganalytics.metadata.modeshape.JcrMetadataAccess; import com.thinkbiganalytics.metadata.modeshape.common.JcrObject; import com.thinkbiganalytics.metadata.modeshape.common.JcrPropertyConstants; import com.thinkbiganalytics.metadata.modeshape.security.JcrAccessControlUtil; import com.thinkbiganalytics.metadata.modeshape.security.mixin.AccessControlledMixin; import com.thinkbiganalytics.metadata.modeshape.support.JcrPropertyUtil; import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil; import com.thinkbiganalytics.security.action.Action; import com.thinkbiganalytics.security.action.AllowableAction; import com.thinkbiganalytics.security.action.AllowedActions; import java.security.AccessControlException; import java.security.Principal; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import javax.jcr.Node; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NodeType; import javax.jcr.security.Privilege; /** * */ public class JcrAllowedActions extends JcrObject implements AllowedActions { public static final String NODE_NAME = "tba:allowedActions"; public static final String NODE_TYPE = "tba:allowedActions"; /** The normal privileges used when a principal is having an action permission granted or revoked */ private static final String[] GRANT_PRIVILEGES = new String[] { Privilege.JCR_READ }; /** The privileges used when a principal is having an action permission granted or revoked that * enables management of the actions (i.e. enable permissions for other principals) */ // TODO: Replace with just JCR_ALL private static final String[] ADMIN_PRIVILEGES = new String[] { Privilege.JCR_READ, Privilege.JCR_READ_ACCESS_CONTROL, Privilege.JCR_MODIFY_ACCESS_CONTROL, Privilege.JCR_ALL }; public JcrAllowedActions(Node allowedActionsNode) { super(allowedActionsNode); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#getAvailableActions() */ @Override public List<AllowableAction> getAvailableActions() { try { NodeType type = JcrUtil.getNodeType(JcrMetadataAccess.getActiveSession(), JcrAllowableAction.NODE_TYPE); return JcrUtil.getJcrObjects(this.node, type, JcrAllowableAction.class).stream().collect(Collectors.toList()); } catch (Exception e) { throw new MetadataException("Failed to retrieve the accessible functons", e); } } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#enableAccess(com.thinkbiganalytics.security.action.Action, java.security.Principal[]) */ @Override public boolean enable(Principal principal, Action action, Action... more) { Set<Action> actions = new HashSet<>(Arrays.asList(more)); actions.add(action); return enable(principal, actions); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#enableAccess(com.thinkbiganalytics.security.action.Action, java.util.Set) */ @Override public boolean enable(Principal principal, Set<Action> actions) { return togglePermission(actions, principal, true); } @Override public boolean enableOnly(Principal principal, Action action, Action... more) { Set<Action> actions = new HashSet<>(Arrays.asList(more)); actions.add(action); return enableOnly(principal, actions); } @Override public boolean enableOnly(Principal principal, Set<Action> actions) { final AtomicBoolean result = new AtomicBoolean(false); getAvailableActions().stream().forEach(available -> { available.stream().forEach(child -> { if (actions.contains(child)) { result.set(togglePermission(child, principal, true) || result.get()); } else { togglePermission(child, principal, false); } }); }); return result.get(); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#enableOnly(java.security.Principal, com.thinkbiganalytics.security.action.AllowedActions) */ @Override public boolean enableOnly(Principal principal, AllowedActions actions) { return enableOnly(principal, actions.getAvailableActions().stream() .flatMap(avail -> avail.stream()) .collect(Collectors.toSet())); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#enableAll(java.security.Principal) */ @Override public boolean enableAll(Principal principal) { return enableOnly(principal, getAvailableActions().stream() .flatMap(avail -> avail.stream()) .collect(Collectors.toSet())); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#disableAccess(com.thinkbiganalytics.security.action.Action, java.security.Principal[]) */ @Override public boolean disable(Principal principal, Action action, Action... more) { Set<Action> actions = new HashSet<>(Arrays.asList(more)); actions.add(action); return disable(principal, actions); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#disableAccess(com.thinkbiganalytics.security.action.Action, java.util.Set) */ @Override public boolean disable(Principal principal, Set<Action> actions) { return togglePermission(actions, principal, false); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#disable(java.security.Principal, com.thinkbiganalytics.security.action.AllowedActions) */ @Override public boolean disable(Principal principal, AllowedActions actions) { return disable(principal, actions.getAvailableActions().stream() .flatMap(avail -> avail.stream()) .collect(Collectors.toSet())); } /* (non-Javadoc) * @see com.thinkbiganalytics.security.action.AllowedActions#deisableAll(java.security.Principal) */ @Override public boolean disableAll(Principal principal) { return disable(principal, getAvailableActions().stream() .flatMap(avail -> avail.stream()) .collect(Collectors.toSet())); } /** * validate a user has a given permission(s) * @param action the action to check * @param more additional actions to check * @return true if user has the permission(s), false if not */ public boolean hasPermission(Action action, Action... more) { Set<Action> actions = new HashSet<>(Arrays.asList(more)); actions.add(action); try { checkPermission(actions); }catch(AccessControlException e){ return false; } return true; } @Override public void checkPermission(Action action, Action... more) { Set<Action> actions = new HashSet<>(Arrays.asList(more)); actions.add(action); checkPermission(actions); } @Override public void checkPermission(Set<Action> actions) { for (Action action : actions) { Node current = getNode(); for (Action parent : action.getHierarchy()) { if (!JcrUtil.hasNode(current, parent.getSystemName())) { throw new AccessControlException("Not authorized to perform the action: " + action.getTitle()); } current = JcrUtil.getNode(current, parent.getSystemName()); } } } public void removeAccessControl(Principal owner) { JcrAccessControlUtil.clearRecursivePermissions(getNode(), JcrAllowableAction.NODE_TYPE); } public void setupAccessControl(Principal owner) { JcrAccessControlUtil.addRecursivePermissions(getNode(), JcrAllowableAction.NODE_TYPE, MetadataAccess.ADMIN, Privilege.JCR_ALL); } public JcrAllowedActions copy(Node allowedNode, Principal principal, String... privilegeNames) { return copy(allowedNode, false, principal, privilegeNames); } public JcrAllowedActions copy(Node allowedNode, boolean includeDescr, Principal principal, String... privilegeNames) { try { // TODO Remove extraneous nodes in destination // for (Node existing : JcrUtil.getIterableChildren(allowedNode)) { // existing.remove(); // } JcrAccessControlUtil.addPermissions(allowedNode, principal, privilegeNames); for (Node actionNode : JcrUtil.getNodesOfType(getNode(), JcrAllowableAction.NODE_TYPE)) { copyAction(actionNode, allowedNode, includeDescr, principal, privilegeNames); } return new JcrAllowedActions(allowedNode); } catch (RepositoryException e) { throw new MetadataException("Failed to copy allowed actions", e); } } protected Set<Action> getEnabledActions(Principal principal) { return getAvailableActions().stream() .flatMap(avail -> avail.stream()) .filter(action -> JcrAccessControlUtil.hasAnyPermission(((JcrAllowableAction) action).getNode(), principal, Privilege.JCR_READ, Privilege.JCR_ALL)) .collect(Collectors.toSet()); } private Node copyAction(Node src, Node destParent, boolean includeDescr, Principal principal, String... privilegeNames) throws RepositoryException { Node dest = JcrUtil.getOrCreateNode(destParent, src.getName(), JcrAllowableAction.NODE_TYPE); if (includeDescr) { JcrPropertyUtil.copyProperty(src, dest, JcrPropertyConstants.TITLE); JcrPropertyUtil.copyProperty(src, dest, JcrPropertyConstants.DESCRIPTION); } JcrAccessControlUtil.addPermissions(dest, principal, privilegeNames); for (Node child : JcrUtil.getNodesOfType(src, JcrAllowableAction.NODE_TYPE)) { copyAction(child, dest, includeDescr, principal, privilegeNames); } return dest; } private boolean togglePermission(Iterable<Action> actions, Principal principal, boolean enable) { boolean result = false; for (Action action : actions) { result |= togglePermission(action, principal, enable); } return result; } private boolean togglePermission(Action action, Principal principal, boolean enable) { // If this actions is a permission management action then grant this principal admin privileges to the whole tree. if (isAdminAction(action)) { if (enable) { return JcrAccessControlUtil.addRecursivePermissions(getNode(), JcrAllowableAction.NODE_TYPE, principal, ADMIN_PRIVILEGES); } else { return JcrAccessControlUtil.removeRecursivePermissions(getNode(), JcrAllowableAction.NODE_TYPE, principal, ADMIN_PRIVILEGES); } } else { return findActionNode(action) .map(node -> { if (enable) { return JcrAccessControlUtil.addHierarchyPermissions(node, principal, this.node, GRANT_PRIVILEGES); } else { return JcrAccessControlUtil.removeRecursivePermissions(node, JcrAllowableAction.NODE_TYPE, principal, GRANT_PRIVILEGES); } }) .orElseThrow(() -> new AccessControlException("Not authorized to " + (enable ? "enable" : "disable") + " the action: " + action)); } } /** * return true if the specified action represents the ability to manage the permissions * of this object on behalf of other principals. The default is false. Subclasses * should override this method to indicate which of their specific actions are * considered admin actions. */ protected boolean isAdminAction(Action action) { // Assume false return false; } private Optional<Node> findActionNode(Action action) { Node current = getNode(); for (Action pathAction : action.getHierarchy()) { if (JcrUtil.hasNode(current, pathAction.getSystemName())) { current = JcrUtil.getNode(current, pathAction.getSystemName()); } else { return Optional.empty(); } } return Optional.of(current); } }