package com.thinkbiganalytics.metadata.modeshape.user;
/*-
* #%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.user.GroupAlreadyExistsException;
import com.thinkbiganalytics.metadata.api.user.User;
import com.thinkbiganalytics.metadata.api.user.UserAlreadyExistsException;
import com.thinkbiganalytics.metadata.api.user.UserGroup;
import com.thinkbiganalytics.metadata.api.user.UserGroup.ID;
import com.thinkbiganalytics.metadata.api.user.UserProvider;
import com.thinkbiganalytics.metadata.modeshape.BaseJcrProvider;
import com.thinkbiganalytics.metadata.modeshape.MetadataRepositoryException;
import com.thinkbiganalytics.metadata.modeshape.common.JcrEntity;
import com.thinkbiganalytics.metadata.modeshape.common.UsersPaths;
import com.thinkbiganalytics.metadata.modeshape.support.JcrQueryUtil;
import com.thinkbiganalytics.metadata.modeshape.support.JcrUtil;
import com.thinkbiganalytics.security.action.AllowedActions;
import com.thinkbiganalytics.security.action.AllowedEntityActionsProvider;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
/**
* Provides access to {@link User} objects stored in a JCR repository.
*/
public class JcrUserProvider extends BaseJcrProvider<Object, Serializable> implements UserProvider {
@Inject
private AllowedEntityActionsProvider actionsProvider;
@Nonnull
@Override
public User ensureUser(@Nonnull final String systemName) {
return createUser(systemName, true);
}
@Nonnull
@Override
public Optional<User> findUserBySystemName(@Nonnull final String systemName) {
final String query = "SELECT * FROM [" + JcrUser.NODE_TYPE + "] AS user WHERE NAME() = $systemName";
final Map<String, String> bindParams = Collections.singletonMap("systemName", encodeUserName(systemName));
return Optional.ofNullable(JcrQueryUtil.findFirst(getSession(), query, bindParams, getEntityClass()));
}
@Override
public Class<? extends User> getEntityClass() {
return JcrUser.class;
}
@Override
public Class<? extends JcrEntity> getJcrEntityClass() {
return JcrUser.class;
}
@Override
public String getNodeType(@Nonnull final Class<? extends JcrEntity> jcrEntityType) {
return JcrUser.NODE_TYPE;
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.BaseProvider#resolveId(java.io.Serializable)
*/
@Override
public User.ID resolveId(@Nonnull final Serializable fid) {
return new JcrUser.UserId(fid);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#resolveUserId(java.io.Serializable)
*/
@Override
public User.ID resolveUserId(@Nonnull Serializable id) {
return new JcrUser.UserId(id);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#resolveGroupId(java.io.Serializable)
*/
@Override
public UserGroup.ID resolveGroupId(@Nonnull Serializable id) {
return new JcrUserGroup.UserGroupId(id);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#userExists(java.lang.String)
*/
@Override
public boolean userExists(@Nonnull String username) {
// TODO: is there a more efficient query than this?
return findUserBySystemName(username).isPresent();
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#findUserById(com.thinkbiganalytics.metadata.api.user.User.ID)
*/
@Nonnull
@Override
public Optional<User> findUserById(@Nonnull User.ID id) {
try {
Node node = getSession().getNodeByIdentifier(id.toString());
if (node.isNodeType(JcrUser.NODE_TYPE)) {
return Optional.of(new JcrUser(node));
} else {
// TODO: should we thrown an exception if the ID is not for a user?
return Optional.empty();
}
} catch (ItemNotFoundException e) {
return Optional.empty();
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed attempting to find the user with ID: " + id, e);
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#getUsers()
*/
@Nonnull
@Override
public Iterable<User> findUsers() {
String query = "SELECT * FROM [" + JcrUser.NODE_TYPE + "]";
return findIterable(query, User.class, JcrUser.class);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#createUser(java.lang.String)
*/
@Nonnull
@Override
public User createUser(@Nonnull String username) {
return createUser(username, false);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#findGroupById(com.thinkbiganalytics.metadata.api.user.UserGroup.ID)
*/
@Nonnull
@Override
public Optional<UserGroup> findGroupById(ID id) {
try {
Node node = getSession().getNodeByIdentifier(id.toString());
if (node.isNodeType(JcrUserGroup.NODE_TYPE)) {
return Optional.of(new JcrUserGroup(node));
} else {
// TODO: should we thrown an exception if the ID is not for a group?
return Optional.empty();
}
} catch (ItemNotFoundException e) {
return Optional.empty();
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed attempting to find the group with ID: " + id, e);
}
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#findGroupByName(java.lang.String)
*/
@Nonnull
@Override
public Optional<UserGroup> findGroupByName(@Nonnull final String groupName) {
final String query = "SELECT * FROM [" + JcrUserGroup.NODE_TYPE + "] AS user WHERE NAME() = $groupName";
final Map<String, String> bindParams = Collections.singletonMap("groupName", encodeGroupName(groupName));
return Optional.ofNullable(JcrQueryUtil.findFirst(getSession(), query, bindParams, JcrUserGroup.class));
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#getGroups()
*/
@Nonnull
@Override
public Iterable<UserGroup> findGroups() {
String query = "SELECT * FROM [" + JcrUserGroup.NODE_TYPE + "]";
return findIterable(query, UserGroup.class, JcrUserGroup.class);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#ensureGroup(java.lang.String)
*/
@Nonnull
@Override
public UserGroup ensureGroup(@Nonnull String groupName) {
return createGroup(groupName, true);
}
/* (non-Javadoc)
* @see com.thinkbiganalytics.metadata.api.user.UserProvider#createGroup(java.lang.String)
*/
@Nonnull
@Override
public UserGroup createGroup(@Nonnull String groupName) {
return createGroup(groupName, false);
}
/**
* Creates a new group with the specified name.
*
* @param groupName the name of the group
* @param ensure {@code true} to return the group if it already exists, or {@code false} to throw an exception
* @return the group
* @throws GroupAlreadyExistsException if the group already exists and {@code ensure} is {@code false}
* @throws MetadataRepositoryException if the group could not be created
*/
@Nonnull
private UserGroup createGroup(@Nonnull final String groupName, final boolean ensure) {
final Session session = getSession();
final String safeGroupName = encodeGroupName(groupName);
final String groupPath = UsersPaths.groupPath(safeGroupName).toString();
try {
final Node groupsNode = session.getRootNode().getNode(UsersPaths.GROUPS.toString());
if (session.getRootNode().hasNode(groupPath)) {
if (ensure) {
return JcrUtil.getJcrObject(groupsNode, safeGroupName, JcrUserGroup.class);
} else {
throw new GroupAlreadyExistsException(groupName);
}
} else {
return JcrUtil.getOrCreateNode(groupsNode, safeGroupName, JcrUserGroup.NODE_TYPE, JcrUserGroup.class);
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed attempting to create a new group with name: " + groupName, e);
}
}
/**
* Creates a new user with the specified name.
*
* @param username the name of the user
* @param ensure {@code true} to return the user if it already exists, or {@code false} to throw an exception
* @return the user
* @throws UserAlreadyExistsException if the user already exists and {@code ensure} is {@code false}
* @throws MetadataRepositoryException if the user could not be created
*/
@Nonnull
private User createUser(@Nonnull final String username, final boolean ensure) {
final Session session = getSession();
final String safeUserName = encodeUserName(username);
final String userPath = UsersPaths.userPath(username).toString();
try {
final Node usersNode = session.getRootNode().getNode(UsersPaths.USERS.toString());
if (session.getRootNode().hasNode(userPath)) {
if (ensure) {
return JcrUtil.getJcrObject(usersNode, safeUserName, JcrUser.class);
} else {
throw new UserAlreadyExistsException(username);
}
} else {
return JcrUtil.getOrCreateNode(usersNode, safeUserName, JcrUser.NODE_TYPE, JcrUser.class);
}
} catch (RepositoryException e) {
throw new MetadataRepositoryException("Failed attempting to create a new user with name: " + username, e);
}
}
@Override
public void deleteGroup(@Nonnull final UserGroup group) {
actionsProvider.getAllowedActions(AllowedActions.SERVICES)
.ifPresent((allowed) -> allowed.disableAll(group.getPrincial()));
delete(group);
}
@Override
public void deleteUser(@Nonnull final User user) {
delete(user);
}
@Nonnull
@Override
public UserGroup updateGroup(@Nonnull final UserGroup group) {
return (UserGroup) update(group);
}
@Nonnull
@Override
public User updateUser(@Nonnull final User user) {
return (User) update(user);
}
/**
* Encodes the specified group name for use with JCR methods.
*
* @param groupName the raw group name
* @return the encoded group name
*/
@Nonnull
private String encodeGroupName(@Nonnull final String groupName) {
try {
return URLEncoder.encode(groupName, JcrUserGroup.ENCODING);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Unsupported encoding for system name of group: " + groupName, e);
}
}
/**
* Encodes the specified user name for use with JCR methods.
*
* @param username the raw user name
* @return the encoded user name
*/
@Nonnull
private String encodeUserName(@Nonnull final String username) {
try {
return URLEncoder.encode(username, JcrUser.ENCODING);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException("Unsupported encoding for system name of user: " + username, e);
}
}
}