de.deepamehta.accesscontrol.AccessControlPlugin Maven / Gradle / Ivy
package de.deepamehta.accesscontrol;
import de.deepamehta.accesscontrol.event.PostLoginUserListener;
import de.deepamehta.accesscontrol.event.PostLogoutUserListener;
import de.deepamehta.config.ConfigCustomizer;
import de.deepamehta.config.ConfigDefinition;
import de.deepamehta.config.ConfigModificationRole;
import de.deepamehta.config.ConfigService;
import de.deepamehta.config.ConfigTarget;
import de.deepamehta.files.FilesService;
import de.deepamehta.files.event.CheckDiskQuotaListener;
import de.deepamehta.workspaces.WorkspacesService;
import de.deepamehta.core.Association;
import de.deepamehta.core.AssociationType;
import de.deepamehta.core.ChildTopics;
import de.deepamehta.core.DeepaMehtaObject;
import de.deepamehta.core.RelatedTopic;
import de.deepamehta.core.Topic;
import de.deepamehta.core.TopicType;
import de.deepamehta.core.ViewConfiguration;
import de.deepamehta.core.model.AssociationModel;
import de.deepamehta.core.model.SimpleValue;
import de.deepamehta.core.model.TopicModel;
import de.deepamehta.core.osgi.PluginActivator;
import de.deepamehta.core.service.DeepaMehtaEvent;
import de.deepamehta.core.service.EventListener;
import de.deepamehta.core.service.Inject;
import de.deepamehta.core.service.Transactional;
import de.deepamehta.core.service.accesscontrol.AccessControl;
import de.deepamehta.core.service.accesscontrol.AccessControlException;
import de.deepamehta.core.service.accesscontrol.Credentials;
import de.deepamehta.core.service.accesscontrol.Operation;
import de.deepamehta.core.service.accesscontrol.Permissions;
import de.deepamehta.core.service.accesscontrol.SharingMode;
import de.deepamehta.core.service.event.CheckAssociationReadAccessListener;
import de.deepamehta.core.service.event.CheckAssociationWriteAccessListener;
import de.deepamehta.core.service.event.CheckTopicReadAccessListener;
import de.deepamehta.core.service.event.CheckTopicWriteAccessListener;
import de.deepamehta.core.service.event.PostCreateAssociationListener;
import de.deepamehta.core.service.event.PostCreateTopicListener;
import de.deepamehta.core.service.event.PostUpdateAssociationListener;
import de.deepamehta.core.service.event.PostUpdateTopicListener;
import de.deepamehta.core.service.event.PreCreateTopicListener;
import de.deepamehta.core.service.event.PreUpdateTopicListener;
import de.deepamehta.core.service.event.ServiceRequestFilterListener;
import de.deepamehta.core.service.event.StaticResourceFilterListener;
import de.deepamehta.core.util.JavaUtils;
// ### TODO: hide Jersey internals. Move to JAX-RS 2.0.
import com.sun.jersey.spi.container.ContainerRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.POST;
import javax.ws.rs.DELETE;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.util.Collection;
import java.util.Enumeration;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
@Path("/accesscontrol")
@Consumes("application/json")
@Produces("application/json")
public class AccessControlPlugin extends PluginActivator implements AccessControlService, ConfigCustomizer,
CheckTopicReadAccessListener,
CheckTopicWriteAccessListener,
CheckAssociationReadAccessListener,
CheckAssociationWriteAccessListener,
PreCreateTopicListener,
PreUpdateTopicListener,
PostCreateTopicListener,
PostCreateAssociationListener,
PostUpdateTopicListener,
PostUpdateAssociationListener,
ServiceRequestFilterListener,
StaticResourceFilterListener,
CheckDiskQuotaListener {
// ------------------------------------------------------------------------------------------------------- Constants
// Security settings
private static final String ANONYMOUS_READ_ALLOWED = System.getProperty("dm4.security.anonymous_read_allowed",
"ALL");
private static final String ANONYMOUS_WRITE_ALLOWED = System.getProperty("dm4.security.anonymous_write_allowed",
"NONE");
private static final AnonymousAccessFilter accessFilter = new AnonymousAccessFilter(ANONYMOUS_READ_ALLOWED,
ANONYMOUS_WRITE_ALLOWED);
private static final String SUBNET_FILTER = System.getProperty("dm4.security.subnet_filter", "127.0.0.1/32");
private static final boolean NEW_ACCOUNTS_ARE_ENABLED = Boolean.parseBoolean(
System.getProperty("dm4.security.new_accounts_are_enabled", "true"));
// Note: the default values are required in case no config file is in effect. This applies when DM is started
// via feature:install from Karaf. The default values must match the values defined in project POM.
private static final boolean IS_PUBLIC_INSTALLATION = ANONYMOUS_READ_ALLOWED.equals("ALL");
private static final String AUTHENTICATION_REALM = "DeepaMehta";
// Type URIs
private static final String LOGIN_ENABLED_TYPE = "dm4.accesscontrol.login_enabled";
private static final String MEMBERSHIP_TYPE = "dm4.accesscontrol.membership";
// Property URIs
private static final String PROP_CREATOR = "dm4.accesscontrol.creator";
private static final String PROP_OWNER = "dm4.accesscontrol.owner";
private static final String PROP_MODIFIER = "dm4.accesscontrol.modifier";
// Events
private static DeepaMehtaEvent POST_LOGIN_USER = new DeepaMehtaEvent(PostLoginUserListener.class) {
@Override
public void dispatch(EventListener listener, Object... params) {
((PostLoginUserListener) listener).postLoginUser(
(String) params[0]
);
}
};
private static DeepaMehtaEvent POST_LOGOUT_USER = new DeepaMehtaEvent(PostLogoutUserListener.class) {
@Override
public void dispatch(EventListener listener, Object... params) {
((PostLogoutUserListener) listener).postLogoutUser(
(String) params[0]
);
}
};
// ---------------------------------------------------------------------------------------------- Instance Variables
@Inject
private WorkspacesService wsService;
@Inject
private FilesService filesService;
@Inject
private ConfigService configService;
@Context
private HttpServletRequest request;
private static Logger logger = Logger.getLogger(AccessControlPlugin.class.getName());
static {
logger.info("Security settings:" +
"\n dm4.security.anonymous_read_allowed = " + accessFilter.dumpReadSetting() +
"\n dm4.security.anonymous_write_allowed = " + accessFilter.dumpWriteSetting() +
"\n dm4.security.subnet_filter = " + SUBNET_FILTER +
"\n dm4.security.new_accounts_are_enabled = " + NEW_ACCOUNTS_ARE_ENABLED);
}
// -------------------------------------------------------------------------------------------------- Public Methods
// *******************************************
// *** AccessControlService Implementation ***
// *******************************************
// === User Session ===
@POST
@Path("/login")
@Override
public void login() {
// Note: the actual login is performed by the request filter. See requestFilter().
}
@POST
@Path("/logout")
@Override
public void logout() {
_logout(request);
//
// For a "private" DeepaMehta installation: emulate a HTTP logout by forcing the webbrowser to bring up its
// login dialog and to forget the former Authorization information. The user is supposed to press "Cancel".
// The login dialog can't be used to login again.
if (!IS_PUBLIC_INSTALLATION) {
throw401Unauthorized(true); // showBrowserLoginDialog=true
}
}
// ---
@GET
@Path("/user")
@Produces("text/plain")
@Override
public String getUsername() {
return dm4.getAccessControl().getUsername(request);
}
@GET
@Path("/username")
@Override
public Topic getUsernameTopic() {
return dm4.getAccessControl().getUsernameTopic(request);
}
// ---
@GET
@Path("/user/workspace")
@Override
public Topic getPrivateWorkspace() {
String username = getUsername();
if (username == null) {
throw new IllegalStateException("No user is logged in");
}
return dm4.getAccessControl().getPrivateWorkspace(username);
}
// === User Accounts ===
@POST
@Path("/user_account")
@Transactional
@Override
public Topic createUserAccount(final Credentials cred) {
try {
final String username = cred.username;
logger.info("Creating user account \"" + username + "\"");
//
// 1) create user account
AccessControl ac = dm4.getAccessControl();
// We suppress standard workspace assignment here as a User Account topic (and its child topics) require
// special assignments. See steps 3) and 4) below.
Topic userAccount = ac.runWithoutWorkspaceAssignment(new Callable() {
@Override
public Topic call() {
return dm4.createTopic(mf.newTopicModel("dm4.accesscontrol.user_account", mf.newChildTopicsModel()
.put("dm4.accesscontrol.username", username)
.put("dm4.accesscontrol.password", cred.password)));
}
});
ChildTopics childTopics = userAccount.getChildTopics();
Topic usernameTopic = childTopics.getTopic("dm4.accesscontrol.username");
Topic passwordTopic = childTopics.getTopic("dm4.accesscontrol.password");
//
// 2) create private workspace
Topic privateWorkspace = wsService.createWorkspace(DEFAULT_PRIVATE_WORKSPACE_NAME, null,
SharingMode.PRIVATE);
setWorkspaceOwner(privateWorkspace, username);
// Note: we don't set a particular creator/modifier here as we don't want suggest that the new user's
// private workspace has been created by the new user itself. Instead we set the *current* user as the
// creator/modifier (via postCreateTopic() listener). In case of the "admin" user account the creator/
// modifier remain undefined as it is actually created by the system itself.
//
// 3) assign user account and password to private workspace
// Note: the current user has no READ access to the private workspace just created.
// So we must use the privileged assignToWorkspace calls here (instead of using the Workspaces service).
long privateWorkspaceId = privateWorkspace.getId();
ac.assignToWorkspace(userAccount, privateWorkspaceId);
ac.assignToWorkspace(passwordTopic, privateWorkspaceId);
//
// 4) assign username to "System" workspace
// Note: user has no READ access to the System workspace. So we must use privileged calls here.
// This is to support the "DM4 Sign-up" 3rd-party plugin.
long systemWorkspaceId = ac.getSystemWorkspaceId();
ac.assignToWorkspace(usernameTopic, systemWorkspaceId);
//
return usernameTopic;
} catch (Exception e) {
throw new RuntimeException("Creating user account \"" + cred.username + "\" failed", e);
}
}
@GET
@Path("/username/{username}")
@Override
public Topic getUsernameTopic(@PathParam("username") String username) {
return dm4.getAccessControl().getUsernameTopic(username);
}
// === Workspaces / Memberships ===
@GET
@Path("/workspace/{workspace_id}/owner")
@Produces("text/plain")
@Override
public String getWorkspaceOwner(@PathParam("workspace_id") long workspaceId) {
// ### TODO: delegate to Core's AccessControl.getOwner()?
return dm4.hasProperty(workspaceId, PROP_OWNER) ? (String) dm4.getProperty(workspaceId, PROP_OWNER) : null;
}
@Override
public void setWorkspaceOwner(Topic workspace, String username) {
try {
workspace.setProperty(PROP_OWNER, username, true); // addToIndex=true
} catch (Exception e) {
throw new RuntimeException("Setting the workspace owner of " + info(workspace) + " failed (username=" +
username + ")", e);
}
}
// ---
@POST
@Path("/user/{username}/workspace/{workspace_id}")
@Transactional
@Override
public void createMembership(@PathParam("username") String username, @PathParam("workspace_id") long workspaceId) {
try {
Association assoc = dm4.createAssociation(mf.newAssociationModel(MEMBERSHIP_TYPE,
mf.newTopicRoleModel(getUsernameTopicOrThrow(username).getId(), "dm4.core.default"),
mf.newTopicRoleModel(workspaceId, "dm4.core.default")
));
assignMembership(assoc);
} catch (Exception e) {
throw new RuntimeException("Creating membership for user \"" + username + "\" and workspace " +
workspaceId + " failed", e);
}
}
@Override
public boolean isMember(String username, long workspaceId) {
return dm4.getAccessControl().isMember(username, workspaceId);
}
// === Permissions ===
@GET
@Path("/topic/{id}")
@Override
public Permissions getTopicPermissions(@PathParam("id") long topicId) {
return getPermissions(topicId);
}
@GET
@Path("/association/{id}")
@Override
public Permissions getAssociationPermissions(@PathParam("id") long assocId) {
return getPermissions(assocId);
}
// === Object Info ===
@GET
@Path("/object/{id}/creator")
@Produces("text/plain")
@Override
public String getCreator(@PathParam("id") long objectId) {
return dm4.getAccessControl().getCreator(objectId);
}
@GET
@Path("/object/{id}/modifier")
@Produces("text/plain")
@Override
public String getModifier(@PathParam("id") long objectId) {
return dm4.hasProperty(objectId, PROP_MODIFIER) ? (String) dm4.getProperty(objectId, PROP_MODIFIER) : null;
}
// === Retrieval ===
@GET
@Path("/creator/{username}/topics")
@Override
public Collection getTopicsByCreator(@PathParam("username") String username) {
return dm4.getTopicsByProperty(PROP_CREATOR, username);
}
@GET
@Path("/owner/{username}/topics")
@Override
public Collection getTopicsByOwner(@PathParam("username") String username) {
return dm4.getTopicsByProperty(PROP_OWNER, username);
}
@GET
@Path("/creator/{username}/assocs")
@Override
public Collection getAssociationsByCreator(@PathParam("username") String username) {
return dm4.getAssociationsByProperty(PROP_CREATOR, username);
}
@GET
@Path("/owner/{username}/assocs")
@Override
public Collection getAssociationsByOwner(@PathParam("username") String username) {
return dm4.getAssociationsByProperty(PROP_OWNER, username);
}
// ****************************
// *** Hook Implementations ***
// ****************************
@Override
public void preInstall() {
configService.registerConfigDefinition(new ConfigDefinition(
ConfigTarget.TYPE_INSTANCES, "dm4.accesscontrol.username",
mf.newTopicModel(LOGIN_ENABLED_TYPE, new SimpleValue(NEW_ACCOUNTS_ARE_ENABLED)),
ConfigModificationRole.ADMIN, this
));
}
@Override
public void shutdown() {
// Note 1: unregistering is crucial e.g. for redeploying the Access Control plugin. The next register call
// (at preInstall() time) would fail as the Config service already holds such a registration.
// Note 2: we must check if the Config service is still available. If the Config plugin is redeployed the
// Access Control plugin is stopped/started as well but at shutdown() time the Config service is already gone.
if (configService != null) {
configService.unregisterConfigDefinition(LOGIN_ENABLED_TYPE);
} else {
logger.warning("Config service is already gone");
}
}
// ****************************************
// *** ConfigCustomizer Implementations ***
// ****************************************
@Override
public TopicModel getConfigValue(Topic topic) {
if (!topic.getTypeUri().equals("dm4.accesscontrol.username")) {
throw new RuntimeException("Unexpected configurable topic: " + topic);
}
// the "admin" account must be enabled regardless of the "dm4.security.new_accounts_are_enabled" setting
if (topic.getSimpleValue().toString().equals(ADMIN_USERNAME)) {
return mf.newTopicModel(LOGIN_ENABLED_TYPE, new SimpleValue(true));
}
// don't customize
return null;
}
// ********************************
// *** Listener Implementations ***
// ********************************
@Override
public void checkTopicReadAccess(long topicId) {
checkReadAccess(topicId);
}
@Override
public void checkTopicWriteAccess(long topicId) {
checkWriteAccess(topicId);
}
// ---
@Override
public void checkAssociationReadAccess(long assocId) {
checkReadAccess(assocId);
//
long[] playerIds = dm4.getPlayerIds(assocId);
checkReadAccess(playerIds[0]);
checkReadAccess(playerIds[1]);
}
@Override
public void checkAssociationWriteAccess(long assocId) {
checkWriteAccess(assocId);
}
// ---
@Override
public void preCreateTopic(TopicModel model) {
if (model.getTypeUri().equals("dm4.accesscontrol.username")) {
String username = model.getSimpleValue().toString();
Topic usernameTopic = getUsernameTopic(username);
if (usernameTopic != null) {
throw new RuntimeException("Username \"" + username + "\" exists already");
}
}
}
@Override
public void postCreateTopic(Topic topic) {
String typeUri = topic.getTypeUri();
if (typeUri.equals("dm4.workspaces.workspace")) {
setWorkspaceOwner(topic);
} else if (typeUri.equals("dm4.webclient.search")) {
// ### TODO: refactoring. The Access Control module must not know about the Webclient.
// Let the Webclient do the workspace assignment instead.
assignSearchTopic(topic);
}
//
setCreatorAndModifier(topic);
}
@Override
public void postCreateAssociation(Association assoc) {
setCreatorAndModifier(assoc);
}
// ---
@Override
public void preUpdateTopic(Topic topic, TopicModel updateModel) {
if (topic.getTypeUri().equals("dm4.accesscontrol.username")) {
SimpleValue newUsername = updateModel.getSimpleValue();
String oldUsername = topic.getSimpleValue().toString();
if (newUsername != null && !newUsername.toString().equals(oldUsername)) {
throw new RuntimeException("A Username can't be changed (tried \"" + oldUsername + "\" -> \"" +
newUsername + "\")");
}
}
}
@Override
public void postUpdateTopic(Topic topic, TopicModel updateModel, TopicModel oldTopic) {
setModifier(topic);
}
@Override
public void postUpdateAssociation(Association assoc, AssociationModel updateModel, AssociationModel oldAssoc) {
if (isMembership(assoc.getModel()) && !isMembership(oldAssoc)) {
assignMembership(assoc);
}
//
setModifier(assoc);
}
// ---
@Override
public void serviceRequestFilter(ContainerRequest containerRequest) {
// Note: we pass the injected HttpServletRequest
requestFilter(request);
}
@Override
public void staticResourceFilter(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
// Note: for the resource filter no HttpServletRequest is injected
requestFilter(servletRequest);
}
// ---
@Override
public void checkDiskQuota(String username, long fileSize, long diskQuota) {
if (diskQuota < 0) {
logger.info("### Checking disk quota of " + userInfo(username) + " SKIPPED -- disk quota is disabled");
return;
}
//
long occupiedSpace = getOccupiedSpace(username);
boolean quotaOK = occupiedSpace + fileSize <= diskQuota;
//
logger.info("### File size: " + fileSize + " bytes, " + userInfo(username) + " occupies " + occupiedSpace +
" bytes, disk quota: " + diskQuota + " bytes => QUOTA " + (quotaOK ? "OK" : "EXCEEDED"));
//
if (!quotaOK) {
throw new RuntimeException("Disk quota of " + userInfo(username) + " exceeded. Disk quota: " +
diskQuota + " bytes. Currently occupied: " + occupiedSpace + " bytes.");
}
}
// ------------------------------------------------------------------------------------------------- Private Methods
private Topic getUsernameTopicOrThrow(String username) {
Topic usernameTopic = getUsernameTopic(username);
if (usernameTopic == null) {
throw new RuntimeException("User \"" + username + "\" does not exist");
}
return usernameTopic;
}
private boolean isMembership(AssociationModel assoc) {
return assoc.getTypeUri().equals(MEMBERSHIP_TYPE);
}
private void assignMembership(Association assoc) {
wsService.assignToWorkspace(assoc, assoc.getTopicByType("dm4.workspaces.workspace").getId());
}
private void assignSearchTopic(Topic searchTopic) {
try {
Topic workspace;
if (getUsername() != null) {
workspace = getPrivateWorkspace();
} else {
workspace = wsService.getWorkspace(WorkspacesService.DEEPAMEHTA_WORKSPACE_URI);
}
wsService.assignToWorkspace(searchTopic, workspace.getId());
} catch (Exception e) {
throw new RuntimeException("Assigning search topic to workspace failed", e);
}
}
// --- Disk quota ---
private long getOccupiedSpace(String username) {
long occupiedSpace = 0;
for (Topic fileTopic : dm4.getTopicsByType("dm4.files.file")) {
long fileTopicId = fileTopic.getId();
if (getCreator(fileTopicId).equals(username)) {
occupiedSpace += filesService.getFile(fileTopicId).length();
}
}
return occupiedSpace;
}
// === Request Filter ===
private void requestFilter(HttpServletRequest request) {
logger.fine("##### " + request.getMethod() + " " + request.getRequestURL() +
"\n ##### \"Authorization\"=\"" + request.getHeader("Authorization") + "\"" +
"\n ##### " + info(request.getSession(false))); // create=false
//
checkRequestOrigin(request); // throws WebApplicationException 403 Forbidden
checkAuthorization(request); // throws WebApplicationException 401 Unauthorized
}
// ---
private void checkRequestOrigin(HttpServletRequest request) {
String remoteAddr = request.getRemoteAddr();
boolean allowed = JavaUtils.isInRange(remoteAddr, SUBNET_FILTER);
//
logger.fine("Remote address=\"" + remoteAddr + "\", dm4.security.subnet_filter=\"" + SUBNET_FILTER +
"\" => " + (allowed ? "ALLOWED" : "FORBIDDEN"));
//
if (!allowed) {
throw403Forbidden(); // throws WebApplicationException
}
}
private void checkAuthorization(HttpServletRequest request) {
if (request.getSession(false) != null) { // create=false
return; // authorized already
}
//
boolean authorized;
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
// Note: if login fails we are NOT authorized, even if no login is required
authorized = tryLogin(new Credentials(authHeader), request);
} else {
authorized = accessFilter.isAnonymousAccessAllowed(request);
}
if (!authorized) {
// Note: a non-public DM installation (anonymous_read_allowed != "ALL") utilizes the browser's login dialog.
// (In contrast a public DM installation utilizes DM's login dialog and must suppress the browser's login
// dialog.)
throw401Unauthorized(!IS_PUBLIC_INSTALLATION); // throws WebApplicationException
}
}
// ---
/**
* Checks weather the credentials are valid and if the user account is enabled, and if both checks are positive
* logs the user in.
*
* @return true if the user has logged in.
*/
private boolean tryLogin(Credentials cred, HttpServletRequest request) {
String username = cred.username;
Topic usernameTopic = checkCredentials(cred);
if (usernameTopic != null && getLoginEnabled(usernameTopic)) {
logger.info("##### Logging in as \"" + username + "\" => SUCCESSFUL!");
_login(username, request);
return true;
} else {
logger.info("##### Logging in as \"" + username + "\" => FAILED!");
return false;
}
}
private Topic checkCredentials(Credentials cred) {
return dm4.getAccessControl().checkCredentials(cred);
}
private boolean getLoginEnabled(Topic usernameTopic) {
Topic loginEnabled = dm4.getAccessControl().getConfigTopic(LOGIN_ENABLED_TYPE, usernameTopic.getId());
return loginEnabled.getSimpleValue().booleanValue();
}
// ---
private void _login(String username, HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("username", username);
logger.info("##### Creating new " + info(session));
//
dm4.fireEvent(POST_LOGIN_USER, username);
}
private void _logout(HttpServletRequest request) {
HttpSession session = request.getSession(false); // create=false
String username = username(session); // save username before invalidating
logger.info("##### Logging out from " + info(session));
//
session.invalidate();
//
dm4.fireEvent(POST_LOGOUT_USER, username);
}
// ---
private String username(HttpSession session) {
return dm4.getAccessControl().username(session);
}
// ---
private void throw401Unauthorized(boolean showBrowserLoginDialog) {
// Note: to suppress the browser's login dialog a contrived authentication scheme "xBasic"
// is used (see http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html)
String authScheme = showBrowserLoginDialog ? "Basic" : "xBasic";
throw new WebApplicationException(Response.status(Status.UNAUTHORIZED)
.header("WWW-Authenticate", authScheme + " realm=" + AUTHENTICATION_REALM)
.header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
.entity("You're not authorized. Sorry.")
.build());
}
private void throw403Forbidden() {
throw new WebApplicationException(Response.status(Status.FORBIDDEN)
.header("Content-Type", "text/html") // for text/plain (default) Safari provides no Web Console
.entity("Access is forbidden. Sorry.")
.build());
}
// === Setup Access Control ===
/**
* Sets the logged in user as the creator/modifier of the given object.
*
* If no user is logged in, nothing is performed.
*/
private void setCreatorAndModifier(DeepaMehtaObject object) {
try {
String username = getUsername();
// Note: when no user is logged in we do NOT fallback to the default user for the access control setup.
// This would not help in gaining data consistency because the topics/associations created so far
// (BEFORE the Access Control plugin is activated) would still have no access control setup.
// Note: for types the situation is different. The type-introduction mechanism (see introduceTopicType()
// handler above) ensures EVERY type is catched (regardless of plugin activation order). For instances on
// the other hand we don't have such a mechanism (and don't want one either).
if (username == null) {
logger.fine("Setting the creator/modifier of " + info(object) + " SKIPPED -- no user is logged in");
return;
}
//
setCreatorAndModifier(object, username);
} catch (Exception e) {
throw new RuntimeException("Setting the creator/modifier of " + info(object) + " failed", e);
}
}
/**
* @param username must not be null.
*/
private void setCreatorAndModifier(DeepaMehtaObject object, String username) {
setCreator(object, username);
setModifier(object, username);
}
// ---
/**
* Sets the creator of a topic or an association.
*/
private void setCreator(DeepaMehtaObject object, String username) {
try {
object.setProperty(PROP_CREATOR, username, true); // addToIndex=true
} catch (Exception e) {
throw new RuntimeException("Setting the creator of " + info(object) + " failed (username=" + username + ")",
e);
}
}
// ---
private void setModifier(DeepaMehtaObject object) {
String username = getUsername();
// Note: when a plugin topic is updated there is no user logged in yet.
if (username == null) {
return;
}
//
setModifier(object, username);
}
private void setModifier(DeepaMehtaObject object, String username) {
object.setProperty(PROP_MODIFIER, username, false); // addToIndex=false
}
// ---
private void setWorkspaceOwner(Topic workspace) {
String username = getUsername();
// Note: username is null if the Access Control plugin is activated already
// when a 3rd-party plugin creates a workspace at install-time.
if (username == null) {
return;
}
//
setWorkspaceOwner(workspace, username);
}
// === Calculate Permissions ===
/**
* @param objectId a topic ID, or an association ID
*/
private void checkReadAccess(long objectId) {
checkAccess(Operation.READ, objectId);
}
/**
* @param objectId a topic ID, or an association ID
*/
private void checkWriteAccess(long objectId) {
checkAccess(Operation.WRITE, objectId);
}
// ---
/**
* @param objectId a topic ID, or an association ID
*/
private void checkAccess(Operation operation, long objectId) {
if (!inRequestScope()) {
logger.fine("### Object " + objectId + " is accessed by \"System\" -- " + operation +
" permission is granted");
return;
}
//
String username = getUsername();
if (!hasPermission(username, operation, objectId)) {
throw new AccessControlException(userInfo(username) + " has no " + operation + " permission for object " +
objectId);
}
}
/**
* @param objectId a topic ID, or an association ID.
*/
private Permissions getPermissions(long objectId) {
return new Permissions().add(Operation.WRITE, hasPermission(getUsername(), Operation.WRITE, objectId));
}
/**
* Checks if a user is permitted to perform an operation on an object (topic or association).
*
* @param username the logged in user, or null
if no user is logged in.
* @param objectId a topic ID, or an association ID.
*
* @return true
if permission is granted, false
otherwise.
*/
private boolean hasPermission(String username, Operation operation, long objectId) {
return dm4.getAccessControl().hasPermission(username, operation, objectId);
}
private boolean inRequestScope() {
try {
request.getMethod();
return true;
} catch (IllegalStateException e) {
// Note: this happens if a request method is called outside request scope.
// This is the case while system startup.
return false;
} catch (NullPointerException e) {
// While system startup request might be null.
// Jersey might not have injected the proxy object yet.
return false;
}
}
// === Logging ===
private String info(DeepaMehtaObject object) {
if (object instanceof TopicType) {
return "topic type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
} else if (object instanceof AssociationType) {
return "association type \"" + object.getUri() + "\" (id=" + object.getId() + ")";
} else if (object instanceof Topic) {
return "topic " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\", uri=\"" + object.getUri() +
"\")";
} else if (object instanceof Association) {
return "association " + object.getId() + " (typeUri=\"" + object.getTypeUri() + "\")";
} else {
throw new RuntimeException("Unexpected object: " + object);
}
}
private String userInfo(String username) {
return "user " + (username != null ? "\"" + username + "\"" : "");
}
private String info(HttpSession session) {
return "session" + (session != null ? " " + session.getId() +
" (username=" + username(session) + ")" : ": null");
}
private String info(HttpServletRequest request) {
StringBuilder info = new StringBuilder();
info.append(" " + request.getMethod() + " " + request.getRequestURI() + "\n");
Enumeration e1 = request.getHeaderNames();
while (e1.hasMoreElements()) {
String name = e1.nextElement();
info.append("\n " + name + ":");
Enumeration e2 = request.getHeaders(name);
while (e2.hasMoreElements()) {
String header = e2.nextElement();
info.append(" " + header);
}
}
return info.toString();
}
}