/**
* PODD is an OWL ontology database used for scientific project management
*
* Copyright (C) 2009-2013 The University Of Queensland
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>.
*/
package com.github.podd.restlet;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Map;
import org.openrdf.OpenRDFException;
import org.openrdf.model.Model;
import org.openrdf.model.URI;
import org.openrdf.model.impl.LinkedHashModel;
import org.openrdf.rio.RDFFormat;
import org.openrdf.rio.RDFParseException;
import org.openrdf.rio.Rio;
import org.openrdf.rio.UnsupportedRDFormatException;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.data.MediaType;
import org.restlet.data.Protocol;
import org.restlet.routing.Router;
import org.restlet.routing.Template;
import org.restlet.routing.TemplateRoute;
import org.restlet.routing.Variable;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.Role;
import org.restlet.security.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.clarkparsia.owlapi.explanation.PelletExplanation;
import com.github.ansell.propertyutil.PropertyUtil;
import com.github.ansell.restletutils.CrossOriginResourceSharingFilter;
import com.github.ansell.restletutils.RestletUtilMediaType;
import com.github.podd.api.PoddArtifactManager;
import com.github.podd.api.PoddRepositoryManager;
import com.github.podd.api.PoddSchemaManager;
import com.github.podd.api.data.PoddDataRepositoryManager;
import com.github.podd.exception.PoddRuntimeException;
import com.github.podd.resources.AboutResourceImpl;
import com.github.podd.resources.AddObjectResourceImpl;
import com.github.podd.resources.ArtifactRolesResourceImpl;
import com.github.podd.resources.CookieLoginResourceImpl;
import com.github.podd.resources.DataReferenceAttachResourceImpl;
import com.github.podd.resources.DeleteArtifactResourceImpl;
import com.github.podd.resources.DeleteObjectResourceImpl;
import com.github.podd.resources.EditArtifactResourceImpl;
import com.github.podd.resources.GetArtifactResourceImpl;
import com.github.podd.resources.GetEventTypeResourceImpl;
import com.github.podd.resources.GetMetadataResourceImpl;
import com.github.podd.resources.GetSchemaResourceImpl;
import com.github.podd.resources.HelpResourceImpl;
import com.github.podd.resources.IndexResourceImpl;
import com.github.podd.resources.ListArtifactsResourceImpl;
import com.github.podd.resources.ListDataRepositoriesResourceImpl;
import com.github.podd.resources.SearchOntologyResourceImpl;
import com.github.podd.resources.SparqlResourceImpl;
import com.github.podd.resources.UploadArtifactResourceImpl;
import com.github.podd.resources.UserAddResourceImpl;
import com.github.podd.resources.UserDetailsResourceImpl;
import com.github.podd.resources.UserEditResourceImpl;
import com.github.podd.resources.UserListResourceImpl;
import com.github.podd.resources.UserPasswordResourceImpl;
import com.github.podd.resources.UserRolesResourceImpl;
import com.github.podd.resources.UserSearchResourceImpl;
import com.github.podd.utils.PODD;
import com.github.podd.utils.PoddRoles;
import com.github.podd.utils.PoddUser;
import com.github.podd.utils.PoddUserStatus;
import com.github.podd.utils.PoddWebConstants;
import freemarker.template.Configuration;
/**
* This class handles all requests from clients to the OAS Web Service.
*
* @author Peter Ansell p_ansell@yahoo.com
*
* Copied from OAS project (https://github.com/ansell/oas)
*
*/
public class PoddWebServiceApplicationImpl extends PoddWebServiceApplication
{
static
{
// Pellet requires this to be called before reasoners are setup to enable tracing for
// inconsistent ontology explanations
PelletExplanation.setup();
}
private final Logger log = LoggerFactory.getLogger(this.getClass());
private volatile Configuration freemarkerConfiguration;
private volatile ChallengeAuthenticator auth;
private volatile PoddSesameRealm realm;
private PoddRepositoryManager poddRepositoryManager;
private PoddSchemaManager poddSchemaManager;
private PoddArtifactManager poddArtifactManager;
private PoddDataRepositoryManager poddDataRepositoryManager;
private Model aliasesConfiguration = new LinkedHashModel();
private PropertyUtil propertyUtil = new PropertyUtil("podd");
/**
* Default Constructor.
*
* Adds the necessary file protocols and sets up the template location.
*
* @throws OpenRDFException
*/
public PoddWebServiceApplicationImpl() throws OpenRDFException
{
super();
this.log.info("\r\n" + "============================== \r\n" + "PODD Web Application \r\n" + "starting... \r\n"
+ "==============================");
// List of protocols required by the application
this.getConnectorService().getClientProtocols().add(Protocol.HTTP);
this.getConnectorService().getClientProtocols().add(Protocol.CLAP);
// Define extensions for RDF and Javascript
// These extensions are also used to identify mediatypes in services
// For example: @Get("owl") will not be processed without the
// declaration below
this.getMetadataService().addExtension("rdf", MediaType.APPLICATION_RDF_XML, true);
this.getMetadataService().addExtension("rj", RestletUtilMediaType.APPLICATION_RDF_JSON, true);
this.getMetadataService().addExtension("owl", MediaType.APPLICATION_RDF_XML, false);
this.getMetadataService().addExtension("json", MediaType.APPLICATION_JSON, true);
this.getMetadataService().addExtension("ttl", MediaType.APPLICATION_RDF_TURTLE, true);
this.getMetadataService().addExtension("n3", MediaType.TEXT_RDF_N3, true);
this.getMetadataService().addExtension("nt", MediaType.TEXT_RDF_NTRIPLES, true);
this.getMetadataService().addExtension("nq",
MediaType.register("text/nquads", "The NQuads extension to the NTriples RDF serialisation"), true);
this.getMetadataService().addExtension("js", MediaType.TEXT_JAVASCRIPT, true);
this.getMetadataService().addExtension("css", MediaType.TEXT_CSS, true);
this.getMetadataService().addExtension("multipart", MediaType.MULTIPART_FORM_DATA, true);
this.getMetadataService().addExtension("form", MediaType.APPLICATION_WWW_FORM, false);
// Automagically tunnel client preferences for extensions through the
// tunnel
this.getTunnelService().setExtensionsTunnel(true);
}
/**
*
*/
@Override
public boolean authenticate(final PoddAction action, final Request request, final Response response,
final URI optionalObjectUri)
{
if(!action.isAuthRequired())
{
return true;
}
else if(!request.getClientInfo().isAuthenticated())
{
if(this.getAuthenticator() == null)
{
throw new RuntimeException("Could not find authentication method");
}
// add challenges to the response and set the status to HTTP 401
// Unauthorized
this.getAuthenticator().challenge(response, false);
// Return false after the challenge and HTTP 401 response have been
// added to the
// response
return false;
}
else if(this.isUserInactive(request.getClientInfo().getUser()))
{
this.log.error("Authenticated user is Inactive. user={}", request.getClientInfo().getUser());
return false;
}
else if(!action.isRoleRequired())
{
return true;
}
else if(request.getClientInfo().getRoles().contains(PoddRoles.ADMIN.getRole()))
{
// All admins can do everything if they are authenticated
return true;
}
else if(!action.matchesForRoles(request.getClientInfo().getRoles()))
{
this.log.error("Authenticated user does not have enough privileges to execute the given action: {}", action);
// FIXME: Implement auditing here
// this.getDataHandler().addLogDetailsForRequest(message,
// referenceUri,
// authenticationScope, get, currentUser, currentRole);
return false;
}
else if(!action.requiresObjectUris(request.getClientInfo().getRoles()))
{
return true;
}
else if(optionalObjectUri == null)
{
this.log.error("Action requires object URIs and none were given: {}", action);
return false;
}
else
{
final Map<String, Collection<Role>> rolesForObjectMap =
this.getRealm().getRolesForObjectAlternate(request.getClientInfo().getUser().getIdentifier(),
optionalObjectUri);
final Collection<Role> rolesCommonAcrossGivenObjects =
rolesForObjectMap.get(request.getClientInfo().getUser().getIdentifier());
if(rolesCommonAcrossGivenObjects == null || !action.matchesForRoles(rolesCommonAcrossGivenObjects))
{
this.log.error("Authenticated user does not have enough privileges to execute the given action: {}"
+ " on the given objects: {}", action, optionalObjectUri);
return false;
}
}
if(request.getClientInfo().isAuthenticated() && request.getClientInfo().getRoles().isEmpty())
{
// TODO: can this case still occur?
this.log.warn("Authenticated user did not have any roles: user={}", request.getClientInfo().getUser());
return false;
}
return true;
}
/**
* Call this method to clean up resources used by PODD. At present it shuts down the Repository.
*/
public void cleanUpResources()
{
try
{
// Avoid NPE if setup failed and we want to shutdown immediately
if(this.getPoddRepositoryManager() != null)
{
// clear all resources and shut down PODD
this.getPoddRepositoryManager().shutDown();
}
}
catch(final OpenRDFException e)
{
this.log.error("Repository Manager could not be shutdown", e);
}
}
/**
* Create the necessary connections between the application and its handlers.
*/
@Override
public Restlet createInboundRoot()
{
final ChallengeAuthenticator authenticator = this.getAuthenticator();
if(authenticator == null)
{
throw new RuntimeException("Could not find authentication method");
}
final Router router = new Router(this.getContext());
// Add a route for Login form. Login service is handled by the
// authenticator
// NOTE: This only displays the login form. All HTTP POST requests to
// the login path should
// be handled by the Authenticator
final String loginFormPath = PoddWebConstants.PATH_LOGIN_FORM;
this.log.debug("attaching login service to path={}", loginFormPath);
router.attach(loginFormPath, CookieLoginResourceImpl.class);
// Add a route for the About page.
final String aboutPagePath = PoddWebConstants.PATH_ABOUT;
this.log.debug("attaching about service to path={}", aboutPagePath);
router.attach(aboutPagePath, AboutResourceImpl.class);
// Add a route for the Help pages.
final String helpOverviewPath = PoddWebConstants.PATH_HELP;
this.log.debug("attaching about service to path={}", helpOverviewPath);
router.attach(helpOverviewPath, HelpResourceImpl.class);
final String helpPagePath = PoddWebConstants.PATH_HELP + "/{" + PoddWebConstants.KEY_HELP_PAGE_IDENTIFIER + "}";
this.log.debug("attaching about service to path={}", helpPagePath);
router.attach(helpPagePath, HelpResourceImpl.class);
// Add a route for the Index page.
final String indexPagePath = PoddWebConstants.PATH_INDEX;
this.log.debug("attaching index service to path={}", indexPagePath);
router.attach(indexPagePath, IndexResourceImpl.class);
// Add a route for the User Details page.
final String userDetailsPath = PoddWebConstants.PATH_USER_DETAILS;
this.log.debug("attaching user details service to path={}", userDetailsPath);
router.attach(userDetailsPath, UserDetailsResourceImpl.class);
// Add a route for List Users page.
final String userListPath = PoddWebConstants.PATH_USER_LIST;
this.log.debug("attaching user list service to path={}", userListPath);
router.attach(userListPath, UserListResourceImpl.class);
// Add a route for Search Users Service.
final String userSearchPath = PoddWebConstants.PATH_USER_SEARCH;
this.log.debug("attaching user search service to path={}", userSearchPath);
router.attach(userSearchPath, UserSearchResourceImpl.class);
// Add a route for Add User page.
final String userAddPath = PoddWebConstants.PATH_USER_ADD;
this.log.debug("attaching user add service to path={}", userAddPath);
router.attach(userAddPath, UserAddResourceImpl.class);
// Add a route for Edit User page.
final String userEditPath = PoddWebConstants.PATH_USER_EDIT;
this.log.debug("attaching user edit service to path={}", userEditPath);
router.attach(userEditPath, UserEditResourceImpl.class);
// Add a route for Change User Password page.
final String userChangePasswordPath = PoddWebConstants.PATH_USER_EDIT_PWD;
this.log.debug("attaching user change password service to path={}", userChangePasswordPath);
router.attach(userChangePasswordPath, UserPasswordResourceImpl.class);
// Add a route for User Roles page.
final String userRolesPath = PoddWebConstants.PATH_USER_ROLES;
this.log.debug("attaching user roles service to path={}", userRolesPath);
router.attach(userRolesPath, UserRolesResourceImpl.class);
// TODO: add routes for other user management pages. (List/Delete Users)
// Add a route for the List Artifacts page.
final String listArtifactsPath = PoddWebConstants.PATH_ARTIFACT_LIST;
this.log.debug("attaching List Artifacts service to path={}", listArtifactsPath);
router.attach(listArtifactsPath, ListArtifactsResourceImpl.class);
// Add a route for the Upload Artifact page.
final String uploadArtifactPath = PoddWebConstants.PATH_ARTIFACT_UPLOAD;
this.log.debug("attaching Upload Artifact service to path={}", uploadArtifactPath);
router.attach(uploadArtifactPath, UploadArtifactResourceImpl.class);
// Add a route for the Get Artifact page.
final String getArtifactBase = PoddWebConstants.PATH_ARTIFACT_GET_BASE;
this.log.debug("attaching Get Artifact (base) service to path={}", getArtifactBase);
router.attach(getArtifactBase, GetArtifactResourceImpl.class);
final String getArtifactInferred = PoddWebConstants.PATH_ARTIFACT_GET_INFERRED;
this.log.debug("attaching Get Artifact (inferred) service to path={}", getArtifactInferred);
router.attach(getArtifactInferred, GetArtifactResourceImpl.class);
// Add a route for the Edit Artifact page.
final String editArtifact = PoddWebConstants.PATH_ARTIFACT_EDIT;
this.log.debug("attaching Edit Artifact service to path={}", editArtifact);
router.attach(editArtifact, EditArtifactResourceImpl.class);
// Add a route for the Artifact Role edit page.
final String artifactRoles = PoddWebConstants.PATH_ARTIFACT_ROLES;
this.log.debug("attaching Edit Artifact Roles service to path={}", artifactRoles);
router.attach(artifactRoles, ArtifactRolesResourceImpl.class);
// Add a route for the Delete Artifact page.
final String deleteArtifact = PoddWebConstants.PATH_ARTIFACT_DELETE;
this.log.debug("attaching Delete Artifact service to path={}", deleteArtifact);
router.attach(deleteArtifact, DeleteArtifactResourceImpl.class);
// Add a route for the Attach File Reference page.
final String attachFileReference = PoddWebConstants.PATH_ATTACH_DATA_REF;
this.log.debug("attaching File Reference Attach service to path={}", attachFileReference);
router.attach(attachFileReference, DataReferenceAttachResourceImpl.class);
// Add a route for the Event page.
final String eventReference = PoddWebConstants.PATH_EVENT_REF;
this.log.debug("attaching Event service to path={}", eventReference);
router.attach(eventReference, GetEventTypeResourceImpl.class);
// Add a route for the List Data Repositories page.
final String listDataRepositories = PoddWebConstants.PATH_DATA_REPOSITORY_LIST;
this.log.debug("attaching List Data Repositories service to path={}", listDataRepositories);
router.attach(listDataRepositories, ListDataRepositoriesResourceImpl.class);
// Add a route for the Search ontology service.
final String searchService = PoddWebConstants.PATH_SEARCH;
this.log.debug("attaching Search Ontology service to path={}", searchService);
router.attach(searchService, SearchOntologyResourceImpl.class);
// Add a route for the Meta-data retrieval service.
final String getMetadataService = PoddWebConstants.PATH_GET_METADATA;
this.log.debug("attaching Metadata service to path={}", getMetadataService);
router.attach(getMetadataService, GetMetadataResourceImpl.class);
// Add a route for the Add Object service.
final String addObjectService = PoddWebConstants.PATH_OBJECT_ADD;
this.log.debug("attaching Add Object service to path={}", addObjectService);
router.attach(addObjectService, AddObjectResourceImpl.class);
// Add a route for the Delete Object page.
final String deleteObject = PoddWebConstants.PATH_OBJECT_DELETE;
this.log.debug("attaching Delete Object service to path={}", deleteObject);
router.attach(deleteObject, DeleteObjectResourceImpl.class);
// Add a route for the Schema retrieval service.
final String getSchemaService = PoddWebConstants.PATH_GET_SCHEMA;
this.log.debug("attaching Schema service to path={}", getSchemaService);
final TemplateRoute schemaService = router.attach(getSchemaService, GetSchemaResourceImpl.class);
schemaService.getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
final Map<String, Variable> routeVariables = schemaService.getTemplate().getVariables();
routeVariables.put("schemaPath", new Variable(Variable.TYPE_URI_PATH));
// Add a route for the SPARQL page.
final String sparqlService = PoddWebConstants.PATH_SPARQL;
this.log.debug("attaching SPARQL service to path={}", sparqlService);
router.attach(sparqlService, SparqlResourceImpl.class);
// Add a route for Logout service
// final String logout = "logout";
// PropertyUtils.getProperty(PropertyUtils.PROPERTY_LOGOUT_FORM_PATH,
// PropertyUtils.DEFAULT_LOGOUT_FORM_PATH);
// this.log.info("attaching logout service to path={}", logout);
// FIXME: Switch between the logout resource implementations here based
// on the authenticator
// router.attach(logout, CookieLogoutResourceImpl.class);
this.log.debug("routes={}", router.getRoutes().toString());
// put the authenticator in front of the resource router so it can
// handle challenge
// responses and forward them on to the right location after locking in
// the authentication
// data. Authentication of individual methods on individual resources is
// handled using calls
// to PoddWebServiceApplication.authenticate()
authenticator.setNext(router);
final CrossOriginResourceSharingFilter corsFilter = new CrossOriginResourceSharingFilter();
corsFilter.setNext(authenticator);
return corsFilter;
}
@Override
public Model getDataRepositoryConfig()
{
// If the aliasConfiguration is empty then populate it with the default
// aliases here
if(this.aliasesConfiguration.isEmpty())
{
final String aliasesFile = this.propertyUtil.get(PODD.KEY_ALIASES, PODD.PATH_DEFAULT_ALIASES_FILE);
try (final InputStream input = this.getClass().getResourceAsStream(aliasesFile);)
{
if(input != null)
{
this.aliasesConfiguration = Rio.parse(input, "", RDFFormat.TURTLE);
}
else
{
this.log.error("Could not find data repository configuration resource: {}", aliasesFile);
}
}
catch(IOException | RDFParseException | UnsupportedRDFormatException e)
{
this.log.error("Could not load data repository configuration: {}", aliasesFile);
throw new PoddRuntimeException("Could not load data repository configuration", e);
}
}
return this.aliasesConfiguration;
}
/**
* Fetches a ChallengeAuthenticator based on the key defined in
* PropertyUtils.PROPERTY_CHALLENGE_AUTH_METHOD.
*
* Currently defaults to a DigestAuthenticator.
*
* @return A ChallengeAuthenticator that can be used to challenge unauthenticated requests to
* resources that need authenticated access.
*/
@Override
public ChallengeAuthenticator getAuthenticator()
{
return this.auth;
}
@Override
public PoddArtifactManager getPoddArtifactManager()
{
return this.poddArtifactManager;
}
@Override
public PoddDataRepositoryManager getPoddDataRepositoryManager()
{
return this.poddDataRepositoryManager;
}
@Override
public PoddRepositoryManager getPoddRepositoryManager()
{
return this.poddRepositoryManager;
}
@Override
public PoddSchemaManager getPoddSchemaManager()
{
return this.poddSchemaManager;
}
@Override
public PropertyUtil getPropertyUtil()
{
return this.propertyUtil;
}
@Override
public PoddSesameRealm getRealm()
{
return this.realm;
}
/*
* (non-Javadoc)
*
* @see net.maenad.oas.webservice.impl.OasWebServiceApplicationInterface#
* getTemplateConfiguration()
*/
@Override
public Configuration getTemplateConfiguration()
{
return this.freemarkerConfiguration;
}
/**
* @param user
* @return false if the User's status is ACTIVE, true in all other cases
*/
private boolean isUserInactive(final User user)
{
if(user == null)
{
return true;
}
if(user instanceof PoddUser)
{
if(((PoddUser)user).getUserStatus() == PoddUserStatus.ACTIVE)
{
return false;
}
}
else
{
final PoddUser findUser = this.getRealm().findUser(user.getIdentifier());
if(findUser.getUserStatus() == PoddUserStatus.ACTIVE)
{
return false;
}
}
return true;
}
@Override
public void setDataRepositoryConfig(final Model aliasesConfiguration)
{
this.aliasesConfiguration = aliasesConfiguration;
}
/**
* @param auth
* the auth to set
*/
@Override
public void setAuthenticator(final ChallengeAuthenticator auth)
{
this.auth = auth;
}
/**
* @param poddArtifactManager
* the poddArtifactManager to set
*/
@Override
public void setPoddArtifactManager(final PoddArtifactManager poddArtifactManager)
{
this.poddArtifactManager = poddArtifactManager;
}
@Override
public void setPoddDataRepositoryManager(final PoddDataRepositoryManager poddDataRepositoryManager)
{
this.poddDataRepositoryManager = poddDataRepositoryManager;
}
/**
* @param poddRepositoryManager
* the poddRepositoryManager to set
*/
@Override
public void setPoddRepositoryManager(final PoddRepositoryManager poddRepositoryManager)
{
this.poddRepositoryManager = poddRepositoryManager;
}
/**
* @param poddSchemaManager
* the poddSchemaManager to set
*/
@Override
public void setPoddSchemaManager(final PoddSchemaManager poddSchemaManager)
{
this.poddSchemaManager = poddSchemaManager;
}
/**
* @param realm
* the realm to set
*/
@Override
public void setRealm(final PoddSesameRealm realm)
{
this.realm = realm;
}
@Override
public void setTemplateConfiguration(final Configuration nextFreemarkerConfiguration)
{
this.freemarkerConfiguration = nextFreemarkerConfiguration;
}
@Override
public void stop() throws Exception
{
super.stop();
this.cleanUpResources();
this.log.info("\r\n" + "============================== \r\n" + "PODD Web Application \r\n"
+ "shutting down... \r\n" + "==============================");
}
}