/**
*
*/
package com.thinkbiganalytics.metadata.modeshape;
/*-
* #%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.MetadataAccess;
import com.thinkbiganalytics.metadata.api.PostMetadataConfigAction;
import com.thinkbiganalytics.metadata.modeshape.common.SecurityPaths;
import com.thinkbiganalytics.metadata.modeshape.extension.ExtensionsConstants;
import com.thinkbiganalytics.metadata.modeshape.security.AdminCredentials;
import com.thinkbiganalytics.metadata.modeshape.security.JcrAccessControlUtil;
import com.thinkbiganalytics.metadata.modeshape.security.ModeShapeAdminPrincipal;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrVersionUtil;
import com.thinkbiganalytics.security.AccessController;
import com.thinkbiganalytics.security.role.SecurityRole;
import org.modeshape.jcr.api.nodetype.NodeTypeManager;
import org.modeshape.jcr.security.SimplePrincipal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeTypeTemplate;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.nodetype.PropertyDefinitionTemplate;
import javax.jcr.security.Privilege;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.VersionManager;
/**
*
*/
public class MetadataJcrConfigurator {
private static final Logger log = LoggerFactory.getLogger(MetadataJcrConfigurator.class);
private final AtomicBoolean configured = new AtomicBoolean(false);
@Inject
private MetadataAccess metadataAccess;
private List<PostMetadataConfigAction> postConfigActions = new ArrayList<>();
public MetadataJcrConfigurator(List<PostMetadataConfigAction> actions) {
this.postConfigActions.addAll(actions);
this.postConfigActions.sort(new AnnotationAwareOrderComparator());
}
public void configure() {
this.metadataAccess.commit(() -> {
try {
Session session = JcrMetadataAccess.getActiveSession();
ensureLayout(session);
ensureTypes(session);
ensureAccessControl(session);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Could not create initial JCR metadata", e);
}
}, MetadataAccess.SERVICE);
this.metadataAccess.commit(() -> {
try {
Session session = JcrMetadataAccess.getActiveSession();
removeVersionableFeedType(session);
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Could remove versioning from feeds", e);
}
}, MetadataAccess.SERVICE);
this.configured.set(true);
firePostConfigActions();
}
private void removeVersionableFeedType(Session session) throws RepositoryException {
Node feedsNode = session.getRootNode().getNode("metadata/feeds");
NodeTypeManager typeMgr = (NodeTypeManager) session.getWorkspace().getNodeTypeManager();
NodeType currentFeedType = typeMgr.getNodeType("tba:feed");
List<String> currentSupertypes = Arrays.asList(currentFeedType.getDeclaredSupertypeNames());
if (currentSupertypes.contains("mix:versionable")) {
log.info("Removing versionable feed type {} ", currentFeedType);
// Remove feed version history
for (Node catNode : JcrUtil.getNodesOfType(feedsNode, "tba:category")) {
for (Node feedNode : JcrUtil.getNodesOfType(catNode, "tba:feed")) {
log.debug("Removing prior versions of feed: {}.{}", catNode.getName(), feedNode.getName());
if (JcrUtil.isVersionable(feedNode)) {
VersionManager versionManager = session.getWorkspace().getVersionManager();
VersionHistory versionHistory = versionManager.getVersionHistory(feedNode.getPath());
VersionIterator vIt = versionHistory.getAllVersions();
int count = 0;
String last = "";
while (vIt.hasNext()) {
Version version = vIt.nextVersion();
String versionName = version.getName();
String baseVersion = "";
if (!"jcr:rootVersion".equals(versionName)) {
//baseVersion requires actual versionable node to get the base version name
baseVersion = JcrVersionUtil.getBaseVersion(feedNode).getName();
}
if (!"jcr:rootVersion".equals(versionName) && !versionName.equalsIgnoreCase(baseVersion)) {
last = version.getName();
// removeVersion writes directly to workspace, no session.save is necessary
versionHistory.removeVersion(version.getName());
count++;
}
}
if (count > 0) {
log.info("Removed {} versions through {} of feed {}", count, last, feedNode.getName());
} else {
log.debug("Feed {} had no versions", feedNode.getName());
}
}
}
}
// Redefine the NodeType of tba:feed to remove versionable but retain the versionable properties with weaker constraints
// Retaining the properties seems to override some residual properties on feed nodes that causes a failure later.
// In particular, jcr:predecessors was accessed later but redefining all mix:versionable properties to be safe.
NodeTypeTemplate template = typeMgr.createNodeTypeTemplate(currentFeedType);
List<String> newSupertypes = currentSupertypes.stream().filter(type -> !type.equals("mix:versionable")).collect(Collectors.toList());
template.setDeclaredSuperTypeNames(newSupertypes.toArray(new String[newSupertypes.size()]));
@SuppressWarnings("unchecked")
List<PropertyDefinitionTemplate> propTemplates = template.getPropertyDefinitionTemplates();
PropertyDefinitionTemplate prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:versionHistory");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
propTemplates.add(prop);
prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:baseVersion");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
propTemplates.add(prop);
prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:predecessors");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
prop.setMultiple(true);
propTemplates.add(prop);
prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:mergeFailed");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
propTemplates.add(prop);
prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:activity");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
propTemplates.add(prop);
prop = typeMgr.createPropertyDefinitionTemplate();
prop.setName("jcr:configuration");
prop.setRequiredType(PropertyType.WEAKREFERENCE);
propTemplates.add(prop);
log.info("Replacing the versionable feed type '{}' with a non-versionable type", currentFeedType);
NodeType newType = typeMgr.registerNodeType(template, true);
log.info("Replaced with new feed type '{}' with a non-versionable type", newType);
// This step may not be necessary.
for (Node catNode : JcrUtil.getNodesOfType(feedsNode, "tba:category")) {
for (Node feedNode : JcrUtil.getNodesOfType(catNode, "tba:feed")) {
feedNode.setPrimaryType(newType.getName());
// log.info("Replaced type of node {}", feedNode);
if (feedNode.hasProperty("jcr:predecessors")) {
feedNode.getProperty("jcr:predecessors").setValue(new Value[0]);
;
feedNode.getProperty("jcr:predecessors").remove();
}
}
}
}
}
private void firePostConfigActions() {
for (PostMetadataConfigAction action : this.postConfigActions) {
// TODO: catch exceptions and continue? Currently propagates runtime exceptions and will fail startup.
action.run();
}
}
public boolean isConfigured() {
return this.configured.get();
}
private void ensureAccessControl(Session session) throws RepositoryException {
if (!session.getRootNode().hasNode(SecurityPaths.SECURITY.toString())) {
session.getRootNode().addNode(SecurityPaths.SECURITY.toString(), "tba:securityFolder");
}
Node prototypesNode = session.getRootNode().getNode(SecurityPaths.PROTOTYPES.toString());
// Uncommenting below will remove all access control action configuration (DEV ONLY.)
// TODO a proper migration should be implemented to in case the action hierarchy
// has changed and the currently permitted actions need to be updated.
// for (Node protoNode : JcrUtil.getNodesOfType(prototypesNode, "tba:allowedActions")) {
// for (Node actionsNode : JcrUtil.getNodesOfType(protoNode, "tba:allowableAction")) {
// actionsNode.remove();
// }
//
// String modulePath = SecurityPaths.moduleActionPath(protoNode.getName()).toString();
//
// if (session.getRootNode().hasNode(modulePath)) {
// Node moduleNode = session.getRootNode().getNode(modulePath);
//
// for (Node actionsNode : JcrUtil.getNodesOfType(moduleNode, "tba:allowableAction")) {
// actionsNode.remove();
// }
// }
// }
JcrAccessControlUtil.addPermissions(prototypesNode, new ModeShapeAdminPrincipal(), Privilege.JCR_ALL);
JcrAccessControlUtil.addPermissions(prototypesNode, AdminCredentials.getPrincipal(), Privilege.JCR_ALL);
JcrAccessControlUtil.addPermissions(prototypesNode, SimplePrincipal.EVERYONE, Privilege.JCR_READ);
}
protected void ensureTypes(Session session) throws RepositoryException {
Node typesNode = session.getRootNode().getNode(ExtensionsConstants.TYPES);
NodeTypeManager typeMgr = (NodeTypeManager) session.getWorkspace().getNodeTypeManager();
NodeTypeIterator typeItr = typeMgr.getPrimaryNodeTypes();
NodeType extensionsType = typeMgr.getNodeType(ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE);
while (typeItr.hasNext()) {
NodeType type = (NodeType) typeItr.next();
if (type.isNodeType(ExtensionsConstants.EXTENSIBLE_ENTITY_TYPE) &&
!type.equals(extensionsType) &&
!typesNode.hasNode(type.getName())) {
Node descrNode = typesNode.addNode(type.getName(), ExtensionsConstants.TYPE_DESCRIPTOR_TYPE);
descrNode.setProperty("jcr:title", simpleName(type.getName()));
descrNode.setProperty("jcr:description", "");
PropertyDefinition[] defs = type.getPropertyDefinitions();
for (PropertyDefinition def : defs) {
String fieldName = def.getName();
String prefix = namePrefix(fieldName);
if (!ExtensionsConstants.STD_PREFIXES.contains(prefix) && !descrNode.hasNode(fieldName)) {
Node propNode = descrNode.addNode(def.getName(), ExtensionsConstants.FIELD_DESCRIPTOR_TYPE);
propNode.setProperty("jcr:title", def.getName().replace("^.*:", ""));
propNode.setProperty("jcr:description", "");
}
}
}
}
NodeIterator nodeItr = typesNode.getNodes();
while (nodeItr.hasNext()) {
Node typeNode = (Node) nodeItr.next();
if (!typeMgr.hasNodeType(typeNode.getName())) {
typeNode.remove();
}
}
}
protected void ensureLayout(Session session) throws RepositoryException {
if (!session.getRootNode().hasNode("metadata")) {
session.getRootNode().addNode("metadata", "tba:metadataFolder");
}
if (!session.getRootNode().hasNode("users")) {
session.getRootNode().addNode("users", "tba:usersFolder");
}
if (!session.getRootNode().hasNode("groups")) {
session.getRootNode().addNode("groups", "tba:groupsFolder");
}
// TODO Temporary to cleanup schemas which had the category folder auto-created.
if (session.getRootNode().hasNode("metadata/feeds/category")) {
session.getRootNode().getNode("metadata/feeds/category").remove();
}
if (!session.getRootNode().hasNode("metadata/hadoopSecurityGroups")) {
session.getRootNode().getNode("metadata").addNode("hadoopSecurityGroups");
}
if (!session.getRootNode().hasNode("metadata/datasourceDefinitions")) {
session.getRootNode().addNode("metadata", "tba:datasourceDefinitionsFolder");
}
if (!session.getRootNode().hasNode("metadata/datasources/derived")) {
if (!session.getRootNode().hasNode("metadata/datasources")) {
session.getRootNode().addNode("metadata", "datasources");
}
session.getRootNode().getNode("metadata/datasources").addNode("derived");
}
if (!session.getRootNode().hasNode("metadata/security")) {
session.getRootNode().addNode("metadata/security", "tba:securityFolder");
}
if (!session.getRootNode().hasNode("metadata/security/prototypes")) {
session.getRootNode().addNode("metadata/security/prototypes", "tba:prototypesFolder");
}
//ensure the datasources exist in prototypes
if (!session.getRootNode().hasNode("metadata/security/prototypes/datasource")) {
session.getRootNode().addNode("metadata/security/prototypes/datasource", "tba:allowedActions");
}
if (!session.getRootNode().hasNode("metadata/security/roles")) {
session.getRootNode().addNode("metadata/security/roles", "tba:rolesFolder");
}
//ensure the role paths exist for the entities
for(String entity : SecurityRole.ENTITIES) {
String entityPath = "metadata/security/roles/"+entity;
if (!session.getRootNode().hasNode(entityPath)) {
session.getRootNode().addNode(entityPath, "tba:rolesFolder");
}
}
}
private String namePrefix(String name) {
Matcher m = ExtensionsConstants.NAME_PATTERN.matcher(name);
if (m.matches()) {
return m.group(1);
} else {
return null;
}
}
private String simpleName(String name) {
Matcher m = ExtensionsConstants.NAME_PATTERN.matcher(name);
if (m.matches()) {
return m.group(2);
} else {
return null;
}
}
}