All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy