/*
* Copyright 2012 SFB 632.
*
* 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.
*/
package annis.service.internal;
import annis.administration.AdministrationDao;
import annis.administration.CorpusAdministration;
import annis.administration.DeleteCorpusDao;
import annis.dao.QueryDao;
import annis.security.ANNISSecurityManager;
import annis.security.ANNISUserConfigurationManager;
import annis.security.ANNISUserRealm;
import annis.security.Group;
import annis.security.User;
import annis.security.UserConfig;
import annis.service.AdminService;
import annis.service.objects.ImportJob;
import annis.utils.ANNISFormatHelper;
import com.google.common.base.Joiner;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.xml.bind.JAXBElement;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.hash.format.Shiro1CryptFormat;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Methods for adminstration.
*
* @author Thomas Krause <krauseto@hu-berlin.de>
*/
@Component
@Path("annis/admin")
public class AdminServiceImpl implements AdminService
{
private final static Logger log = LoggerFactory.getLogger(
AdminServiceImpl.class);
private AdministrationDao adminDao;
private DeleteCorpusDao deleteCorpusDao;
private CorpusAdministration corpusAdmin;
private QueryDao queryDao;
private ImportWorker importWorker;
@Context
HttpServletRequest request;
public void init()
{
// check scheme at each service startup
if(corpusAdmin.getSchemeFixer() != null)
{
corpusAdmin.getSchemeFixer().checkAndFix();
}
importWorker.start();
}
@GET
@Path("is-authenticated")
@Produces("text/plain")
public Response isAuthenticated()
{
Subject user = SecurityUtils.getSubject();
Object principal = user.getPrincipal();
if(principal instanceof String)
{
// if a use has an expired account it won't have it's own name as role
boolean hasOwnRole = user.hasRole((String) principal);
if(!hasOwnRole)
{
return Response.status(Response.Status.FORBIDDEN).entity("Account expired").build();
}
}
return Response.ok(Boolean.toString(user.isAuthenticated())).build();
}
/**
* Get the user configuration for the currently logged in user.
*
* @return
*/
@GET
@Path("userconfig")
@Produces("application/xml")
public UserConfig getUserConfig()
{
Subject user = SecurityUtils.getSubject();
user.checkPermission("admin:read:userconfig");
return adminDao.retrieveUserConfig((String) user.getPrincipal());
}
/**
* Sets the user configuration for the currently logged in user.
*/
@POST
@Path("userconfig")
@Consumes("application/xml")
public Response setUserConfig(JAXBElement<UserConfig> config)
{
Subject user = SecurityUtils.getSubject();
user.checkPermission("admin:write:userconfig");
String userName = (String) user.getPrincipal();
adminDao.storeUserConfig(userName, config.getValue());
return Response.ok().build();
}
@GET
@Path("users")
@Produces("application/xml")
public List<User> listUsers()
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:read:user");
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager = getConfManager();
if (confManager != null)
{
return confManager.listAllUsers();
}
}
return new LinkedList<>();
}
@PUT
@Path("users/{userName}")
@Consumes("application/xml")
@Override
public Response updateOrCreateUser(
User user,
@PathParam("userName") String userName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:user");
if (!userName.equals(user.getName()))
{
return Response.status(Response.Status.BAD_REQUEST)
.entity("Username in object is not the same as in path")
.build();
}
// if any permission is an adminstrative one the
// requesting user needs more than just a "admin:write:user" permission"
for(String permission : user.getPermissions())
{
if(permission.startsWith("admin:"))
{
requestingUser.checkPermission("admin:write:adminuser");
break;
}
}
ANNISUserRealm userRealm = getUserRealm();
if (userRealm != null)
{
if (userRealm.updateUser(user))
{
return Response.ok().build();
}
}
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Could not update/create user")
.build();
}
@GET
@Path("users/{userName}")
@Produces("application/xml")
@Override
public User getUser(@PathParam("userName")
String userName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:read:user");
ANNISUserConfigurationManager conf = getConfManager();
if(conf != null)
{
User u = conf.getUser(userName);
if(u == null)
{
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
// remove the password hash from the result, we don't want someone with
// lower adminstration rights to crack it
u.setPasswordHash("");
return u;
}
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
@DELETE
@Path("users/{userName}")
public Response deleteUser(@PathParam("userName") String userName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:user");
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager = getConfManager();
if (confManager != null)
{
if (confManager.deleteUser(userName))
{
// also delete any possible user configs
adminDao.deleteUserConfig(userName);
// if no error until here everything went well
return Response.ok().build();
}
}
}
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Could not delete user")
.build();
}
@POST
@Path("users/{userName}/password")
@Consumes("text/plain")
@Produces("application/xml")
public Response changePassword(
String newPassword,
@PathParam("userName") String userName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:user");
ANNISUserConfigurationManager confManager = getConfManager();
ANNISUserRealm userRealm = getUserRealm();
if (confManager != null && userRealm != null)
{
User user = confManager.getUser(userName);
if (user == null)
{
return Response.status(Response.Status.NOT_FOUND).build();
}
Shiro1CryptFormat format = new Shiro1CryptFormat();
SecureRandomNumberGenerator generator
= new SecureRandomNumberGenerator();
ByteSource salt = generator.nextBytes(128/8); // 128 bit
Sha256Hash hash = new Sha256Hash(newPassword, salt, 1);
user.setPasswordHash(format.format(hash));
if (userRealm.updateUser(user))
{
return Response.ok().entity(user).build();
}
}
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Could not change password")
.build();
}
@GET
@Path("groups")
@Produces("application/xml")
public List<Group> listGroups()
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:read:group");
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager = getConfManager();
if (confManager != null)
{
return new LinkedList<>(confManager.getGroups().values());
}
}
return new LinkedList<>();
}
@PUT
@Path("groups/{groupName}")
@Consumes("application/xml")
public Response updateOrCreateGroup(
Group group,
@PathParam("groupName") String groupName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:group");
if (!groupName.equals(group.getName()))
{
return Response.status(Response.Status.BAD_REQUEST)
.entity("Group name in object is not the same as in path")
.build();
}
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager = getConfManager();
if (confManager != null)
{
if (confManager.writeGroup(group))
{
return Response.ok().build();
}
}
}
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Could not update/create group")
.build();
}
@DELETE
@Path("groups/{groupName}")
public Response deleteGroup(@PathParam("groupName") String groupName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:group");
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager = getConfManager();
if (confManager != null)
{
if (confManager.deleteGroup(groupName))
{
return Response.ok().build();
}
}
}
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Could not delete group")
.build();
}
@DELETE
@Path("corpora/{corpusName}")
public Response deleteCorpus(@PathParam("corpusName") String corpusName)
{
Subject requestingUser = SecurityUtils.getSubject();
requestingUser.checkPermission("admin:write:corpus");
try
{
// get ID of corpus
long id = queryDao.mapCorpusNameToId(corpusName);
deleteCorpusDao.deleteCorpora(Arrays.asList(id), true);
return Response.status(Response.Status.OK).build();
}
catch (IllegalArgumentException ex)
{
return Response.status(Response.Status.NOT_FOUND).build();
}
}
@GET
@Path("import/status")
@Override
public List<ImportJob> currentImports()
{
Subject user = SecurityUtils.getSubject();
user.checkPermission("admin:query-import:running");
List<ImportJob> result = new LinkedList<>();
ImportJob current = importWorker.getCurrentJob();
if (current != null
&& current.getStatus() != ImportJob.Status.SUCCESS && current.getStatus() != ImportJob.Status.ERROR)
{
result.add(current);
}
result.addAll(importWorker.getImportQueue());
return result;
}
@GET
@Path("import/status/finished/{uuid}")
@Override
public ImportJob finishedImport(@PathParam("uuid") String uuid)
{
Subject user = SecurityUtils.getSubject();
user.checkPermission("admin:query-import:finished");
ImportJob job = importWorker.getFinishedJob(uuid);
if (job == null)
{
throw new WebApplicationException(404);
}
return job;
}
@POST
@Path("import")
@Consumes(
{
"application/zip"
})
@Override
public Response importCorpus(
@QueryParam("overwrite") String overwriteRaw,
@QueryParam("statusMail") String statusMail,
@QueryParam("alias") String alias)
{
Subject user = SecurityUtils.getSubject();
boolean overwrite = Boolean.parseBoolean(overwriteRaw);
// write content to temporary file
try
{
File tmpZip = File.createTempFile("annis-import", ".zip");
tmpZip.deleteOnExit();
try (OutputStream tmpOut = new FileOutputStream(tmpZip))
{
ByteStreams.copy(request.getInputStream(), tmpOut);
}
Set<String> allNames = ANNISFormatHelper.corporaInZipfile(tmpZip).keySet();
if (!allNames.isEmpty())
{
for (String corpusName : allNames)
{
user.checkPermission("admin:import:" + corpusName);
}
String caption = Joiner.on(", ").join(allNames);
List<Long> corpusIDs = queryDao.mapCorpusNamesToIds(new LinkedList<>(
allNames));
if (overwrite || corpusIDs == null || corpusIDs.isEmpty())
{
ImportJob job = new ImportJob();
UUID uuid = UUID.randomUUID();
job.setUuid(uuid.toString());
job.setCaption(caption);
job.setImportRootDirectory(tmpZip);
job.setStatus(ImportJob.Status.WAITING);
job.setOverwrite(overwrite);
job.setStatusEmail(statusMail);
job.setAlias(alias);
corpusAdmin.sendImportStatusMail(statusMail, caption,
ImportJob.Status.WAITING, null);
try
{
importWorker.getImportQueue().put(job);
return Response.status(Response.Status.ACCEPTED).header("Location",
request.getContextPath() + "/annis/admin/import/status/finished/" + uuid.
toString())
.build();
}
catch (InterruptedException ex)
{
log.error("Could not add job to import queue", ex);
return Response.serverError().entity("Could not add job to "
+ "import queue. There might be more information in the server "
+ "log files. Contact the administrator if necessary.").build();
}
}
else
{
return Response.status(Response.Status.BAD_REQUEST)
.entity("The corpus already exists").build();
}
}
else
{
return Response.status(Response.Status.BAD_REQUEST)
.entity("no corpus.tab file found in upload")
.build();
}
}
catch (IOException ex)
{
log.error(null, ex);
}
return Response.serverError().build();
}
private ANNISUserConfigurationManager getConfManager()
{
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserConfigurationManager confManager
= ((ANNISSecurityManager) SecurityUtils.getSecurityManager()).
getConfManager();
return confManager;
}
return null;
}
private ANNISUserRealm getUserRealm()
{
if (SecurityUtils.getSecurityManager() instanceof ANNISSecurityManager)
{
ANNISUserRealm userRealm
= ((ANNISSecurityManager) SecurityUtils.getSecurityManager()).getANNISUserRealm();
return userRealm;
}
return null;
}
public AdministrationDao getAdminDao()
{
return adminDao;
}
public void setAdminDao(AdministrationDao adminDao)
{
this.adminDao = adminDao;
}
public QueryDao getQueryDao()
{
return queryDao;
}
public void setQueryDao(QueryDao queryDao)
{
this.queryDao = queryDao;
}
public ImportWorker getImportWorker()
{
return importWorker;
}
public void setImportWorker(ImportWorker importWorker)
{
this.importWorker = importWorker;
}
public CorpusAdministration getCorpusAdmin()
{
return corpusAdmin;
}
public void setCorpusAdmin(CorpusAdministration corpusAdmin)
{
this.corpusAdmin = corpusAdmin;
}
public DeleteCorpusDao getDeleteCorpusDao()
{
return deleteCorpusDao;
}
public void setDeleteCorpusDao(DeleteCorpusDao deleteCorpusDao)
{
this.deleteCorpusDao = deleteCorpusDao;
}
}