sernet.verinice.service.XmlRightsService.java Source code

Java tutorial

Introduction

Here is the source code for sernet.verinice.service.XmlRightsService.java

Source

/*******************************************************************************
 * Copyright (c) 2011 Daniel Murygin.
 *
 * This program is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. 
 * If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Daniel Murygin <dm[at]sernet[dot]de> - initial API and implementation
 ******************************************************************************/
package sernet.verinice.service;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.springframework.core.io.Resource;

import sernet.gs.service.SecurityException;
import sernet.hui.common.connect.Property;
import sernet.verinice.interfaces.ActionRightIDs;
import sernet.verinice.interfaces.IAuthService;
import sernet.verinice.interfaces.IBaseDao;
import sernet.verinice.interfaces.IConfigurationService;
import sernet.verinice.interfaces.IRightsChangeListener;
import sernet.verinice.interfaces.IRightsService;
import sernet.verinice.model.auth.Action;
import sernet.verinice.model.auth.Auth;
import sernet.verinice.model.auth.ConfigurationType;
import sernet.verinice.model.auth.OriginType;
import sernet.verinice.model.auth.Profile;
import sernet.verinice.model.auth.ProfileRef;
import sernet.verinice.model.auth.Profiles;
import sernet.verinice.model.auth.Userprofile;
import sernet.verinice.model.auth.Userprofiles;
import sernet.verinice.model.bsi.Person;
import sernet.verinice.model.common.CnATreeElement;
import sernet.verinice.model.common.configuration.Configuration;
import sernet.verinice.model.iso27k.PersonIso;

/**
 * Service to read and change the authorization configuration of verinice.
 * 
 * This implementation loads and saves configuration in an XML file.
 * XML schema is defined in: <code>verinice-auth.xsd</code>.
 * 
 * Configuration is defined in two documents:
 * <ul>
 * <li>WEB-INF/verinice-auth-default.xml: Default configuration. This file is never changed by an administrator.</li>
 * <li>WEB-INF/verinice-auth.xml: Configuration. Settings in this file overwrite verinice-auth-default.xml.</li>
 * </ul>
 * 
 * This service is managed by the Spring container as a _singleton_ but it's running
 * in a multi threaded web server environment. Keep that in mind if you change this service.
 * 
 * @author Daniel Murygin <dm[at]sernet[dot]de>
 */
public class XmlRightsService implements IRightsService {

    private final Logger log = Logger.getLogger(XmlRightsService.class);

    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock readLock = readWriteLock.readLock();
    private final Lock writeLock = readWriteLock.writeLock();

    /**
     * Holds the authorization configuration.
     * Variable will be read and modified by different threads.
     * Read this for a definition of "volatile":
     * http://www.javamex.com/tutorials/synchronization_volatile.shtml
     */
    private volatile Auth auth;

    /**
     * Key: scope name, Value: all users of this scope
     */
    private volatile Map<String, List<String>> usernameMap = new Hashtable<String, List<String>>();

    /**
     * Key: scope name, Value: all groups of this scope
     */
    private volatile Map<String, List<String>> groupnameMap = new Hashtable<String, List<String>>();

    private RightsServerHandler rightsServerHandler;

    private Resource authConfigurationDefault;

    private Resource authConfiguration;

    private Resource authConfigurationSchema;

    private JAXBContext context;

    private Schema schema;

    private IConfigurationService configurationService;

    private IBaseDao<Configuration, Integer> configurationDao;

    private IBaseDao<Property, Integer> propertyDao;

    private IRemoteMessageSource messages;

    private IAuthService authService;

    private Map<String, Profile> profileMap;

    private static List<IRightsChangeListener> changeListener = new LinkedList<IRightsChangeListener>();

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getConfiguration()
     */
    @Override
    public Auth getConfiguration() {
        // a local var. is used to make this thread save:
        Auth currentAuth = auth;
        if (currentAuth == null) {
            // prevent reading the configuration while another thread is writing it
            readLock.lock();
            try {
                currentAuth = loadConfiguration();
                auth = currentAuth;
            } finally {
                readLock.unlock();
            }

            if (log.isDebugEnabled()) {
                if (log.isDebugEnabled()) {
                    log.debug("Merged auth configuration: ");
                }
                logAuth(currentAuth);
            }
        }
        return currentAuth;
    }

    /**
     * For debugging only!
     */
    private void logAuth(Auth auth) {
        try {
            if (log.isDebugEnabled()) {
                Marshaller marshaller = getContext().createMarshaller();
                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
                marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
                StringWriter sw = new StringWriter();
                marshaller.marshal(auth, sw);
                log.debug(sw.toString());
            }
        } catch (Exception e) {
            log.error("Error while logging auth", e);
        }
    }

    /**
     * Loads the configuration by merging the files
     * 'verinice-auth-default.xml' and 'verinice-auth.xml'
     * 
     * @return The authorization configuration
     * @throws IllegalAuthConfTypeException if a different configurationType is detected in 'verinice-auth-default.xml' and 'verinice-auth.xml'
     */
    private Auth loadConfiguration() {
        try {
            Unmarshaller unmarshaller = getContext().createUnmarshaller();
            unmarshaller.setSchema(getSchema());

            // read default configuration
            Auth authDefault = (Auth) unmarshaller.unmarshal(getAuthConfigurationDefault().getInputStream());
            Auth authUser = null;

            // check if configuration exists
            if (getAuthConfiguration().exists()) {
                if (log.isDebugEnabled()) {
                    log.debug("Reading authorization configuration from file: "
                            + getAuthConfiguration().getFile().getPath());
                }

                authUser = (Auth) unmarshaller.unmarshal(getAuthConfiguration().getInputStream());

                // check configuration type of both files
                // throw an exception if a different type is detected
                if (!authDefault.getType().equals(authUser.getType())) {
                    final String message = "You must use the same configurationType in 'verinice-auth-default.xml' and 'verinice-auth.xml'";
                    throw new IllegalAuthConfTypeException(message);
                }
                // merge both configurations
                authDefault = AuthHelper.merge(new Auth[] { authUser, authDefault });
            }

            return authDefault;
        } catch (RuntimeException e) {
            log.error("Error while reading verinice authorization definition from file: "
                    + getAuthConfiguration().getFilename(), e);
            throw e;
        } catch (Exception e) {
            log.error("Error while reading verinice authorization definition from file: "
                    + getAuthConfiguration().getFilename(), e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Updates the configuration defined in <code>auth</code>.
     * 
     * Content of <code>authNew</code> is saved in file: verinice-auth.xml
     * if origin of profiles and userprofiles is "modification".
     * 
     * Before writing the content the implementation checks
     * if user is allowed to change the configuration.
     * 
     * @see sernet.verinice.interfaces.IRightsService#updateConfiguration(sernet.verinice.model.auth.Auth)
     */
    @Override
    public void updateConfiguration(Auth authNew) {
        try {
            if (!isReferenced(ActionRightIDs.EDITPROFILE, authNew)) {
                log.warn("Right id: " + ActionRightIDs.EDITPROFILE
                        + " is not referenced in the auth configuration. No user is able to change the configuration anymore.");
            }

            Profiles profilesMod = new Profiles();
            for (Profile profile : authNew.getProfiles().getProfile()) {
                // add profile if origin is "modification"
                if (!OriginType.DEFAULT.equals(profile.getOrigin())) {
                    profilesMod.getProfile().add(profile);
                }
            }
            authNew.setProfiles(profilesMod);

            Userprofiles userprofilesMod = new Userprofiles();

            for (Userprofile userprofile : authNew.getUserprofiles().getUserprofile()) {
                if (!OriginType.DEFAULT.equals(userprofile.getOrigin())) {
                    userprofilesMod.getUserprofile().add(userprofile);
                }
            }
            authNew.setUserprofiles(userprofilesMod);

            // Block all other threads before writing the file
            writeLock.lock();
            try {
                checkWritePermission(); //throws sernet.gs.service.SecurityException
                // create a backup of the old configuration
                backupConfigurationFile();
                // write the new configuration
                Marshaller marshaller = getContext().createMarshaller();
                marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
                marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
                marshaller.setSchema(getSchema());
                marshaller.marshal(authNew, new FileOutputStream(getAuthConfiguration().getFile().getPath()));
                // set auth to null, 
                // next call of getCofiguration will read the new configuration from disk
                this.auth = null;
            } finally {
                writeLock.unlock();
            }

            fireChangeEvent();

        } catch (sernet.gs.service.SecurityException e) {
            log.error(e.getMessage(), e);
            throw e;
        } catch (Exception e) {
            String message = "Error while updating authorization configuration.";
            log.error(message, e);
            // Big Problem: writing of configuration failed! 
            // Restore it from backup
            // Block all other threads before writing the file
            writeLock.lock();
            try {
                log.error("Trying to restore the authorization configuration from backup file now...");
                restoreConfigurationFile();
                log.error("Authorization configuration restored from backup file.");
            } finally {
                writeLock.unlock();
            }
            throw new RuntimeException(message);
        }
    }

    private void fireChangeEvent() {
        for (IRightsChangeListener listener : getChangeListener()) {
            listener.configurationChanged(getConfiguration());
        }
    }

    /**
     * Throws a sernet.gs.service.SecurityException
     * if current user has no permission to write the 
     * authorization configuration.
     */
    private void checkWritePermission() throws sernet.gs.service.SecurityException {
        boolean isWritePermission = getRightsServerHandler().isEnabled(getAuthService().getUsername(),
                ActionRightIDs.EDITPROFILE);
        if (!isWritePermission) {
            throw new SecurityException("User " + getAuthService().getUsername()
                    + " has no permission to write authorization configuration.");
        }
    }

    /**
     * Creates a copy of the configuration file with suffix ".bak".
     * If the copy file exists, then this method will overwrite it. 
     * @throws IOException 
     */
    private void backupConfigurationFile() {
        try {
            File backup = new File(getBackupFileName());
            FileUtils.copyFile(getAuthConfiguration().getFile(), backup);
        } catch (Exception t) {
            log.error("Error while creating backup of authorization configuration.", t);
        }
    }

    /**
     * Restores the configuration file from the backup with suffix ".bak".
     */
    private void restoreConfigurationFile() {
        try {
            File backup = new File(getBackupFileName());
            File conf = getAuthConfiguration().getFile();
            FileUtils.copyFile(backup, conf);
        } catch (Exception t) {
            log.error("Error while restoring authorization configuration.", t);
        }
    }

    private String getBackupFileName() throws IOException {
        return getAuthConfiguration().getFile().getAbsolutePath() + ".bak";
    }

    /**
     * Returns the userprofiles of an user by selecting the 
     * userprofile of the user and the userprofiles of the
     * groups the user belongs to.
     * 
     * @see sernet.verinice.interfaces.IRightsService#getUserprofile(java.lang.String)
     */
    @Override
    public List<Userprofile> getUserprofile(String username) {
        List<String> roleList = getRoleList(username);
        // add the username to the list
        roleList.add(username);
        List<Userprofile> userprofileList = new ArrayList<Userprofile>(1);
        List<Userprofile> allUserprofileList = getConfiguration().getUserprofiles().getUserprofile();
        for (Userprofile userprofile : allUserprofileList) {
            if (roleList.contains(userprofile.getLogin())) {
                userprofileList.add(userprofile);
            }
        }
        return userprofileList;
    }

    /**
     * Returns an list with the role/groups of an user. 
     * The returned list contains the user name.
     * 
     * @param username an username
     * @return the role/groups of an user
     */
    private List<String> getRoleList(String username) {
        // select all groups of the user
        String hql = "select roleprops.propertyValue from Configuration as conf " + //$NON-NLS-1$
                "inner join conf.entity as entity " + //$NON-NLS-1$
                "inner join entity.typedPropertyLists as propertyList " + //$NON-NLS-1$
                "inner join propertyList.properties as props " + //$NON-NLS-1$
                "inner join conf.entity as entity2 " + //$NON-NLS-1$
                "inner join entity2.typedPropertyLists as propertyList2 " + //$NON-NLS-1$
                "inner join propertyList2.properties as roleprops " + //$NON-NLS-1$
                "where props.propertyType = ? " + //$NON-NLS-1$
                "and props.propertyValue like ? " + //$NON-NLS-1$
                "and roleprops.propertyType = ?"; //$NON-NLS-1$
        String escaped = username.replace("\\", "\\\\");
        Object[] params = new Object[] { Configuration.PROP_USERNAME, escaped, Configuration.PROP_ROLES };
        return getConfigurationDao().findByQuery(hql, params);
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getUsernames()
     */
    @Override
    public List<String> getUsernames() {
        String hql = "select props.propertyValue from Property as props " + //$NON-NLS-1$
                "where props.propertyType = ?"; //$NON-NLS-1$
        Object[] params = new Object[] { Configuration.PROP_USERNAME };
        List<String> usernameList = getPropertyDao().findByQuery(hql, params);
        usernameList.add(getAuthService().getAdminUsername());
        return usernameList;
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getGroupnames()
     */
    @Override
    public List<String> getGroupnames() {
        String hql = "select props.propertyValue from Property as props " + //$NON-NLS-1$
                "where props.propertyType = ?"; //$NON-NLS-1$
        Object[] params = new Object[] { Configuration.PROP_ROLES };
        return getPropertyDao().findByQuery(hql, params);
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getProfiles(java.lang.Integer)
     */
    @Override
    public List<String> getGroupnames(String username) {
        List<String> groupnameList = groupnameMap.get(username);
        if (groupnameList == null) {
            loadUserAndGroupNames(username);
            groupnameList = groupnameMap.get(username);
        }
        return groupnameList;
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getUsernames(java.lang.String)
     */
    @Override
    public List<String> getUsernames(String username) {
        List<String> usernameList = usernameMap.get(username);
        if (usernameList == null) {
            loadUserAndGroupNames(username);
            usernameList = usernameMap.get(username);
        }
        return usernameList;
    }

    private void loadUserAndGroupNames(String username) {
        Integer scopeId = getConfigurationService().getScopeId(username);

        String hql = "from CnATreeElement c " + //$NON-NLS-1$           
                "where c.scopeId = ? " + //$NON-NLS-1$
                "and (c.objectType = ? or c.objectType = ?)"; //$NON-NLS-1$
        Object[] params = new Object[] { scopeId, PersonIso.TYPE_ID, Person.TYPE_ID };
        List<CnATreeElement> elementList = getPropertyDao().findByQuery(hql, params);
        Object[] idList = new Object[elementList.size()];
        int i = 0;
        for (CnATreeElement person : elementList) {
            idList[i] = person.getDbId();
            i++;
        }
        DetachedCriteria crit = DetachedCriteria.forClass(Configuration.class);
        crit.setFetchMode("entity", FetchMode.JOIN); //$NON-NLS-1$
        crit.setFetchMode("entity.typedPropertyLists", FetchMode.JOIN); //$NON-NLS-1$
        crit.setFetchMode("entity.typedPropertyLists.properties", FetchMode.JOIN); //$NON-NLS-1$
        crit.setFetchMode("person", FetchMode.JOIN); //$NON-NLS-1$
        crit.add(Restrictions.in("person.id", idList)); //$NON-NLS-1$
        crit.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

        List<Configuration> confList = getPropertyDao().findByCriteria(crit);
        Set<String> usernameList = new HashSet<String>(confList.size());
        Set<String> groupnameList = new HashSet<String>(confList.size());
        for (Configuration configuration : confList) {
            if (configuration.getUser() != null && !configuration.getUser().trim().isEmpty()) {
                usernameList.add(configuration.getUser());
            }
            groupnameList.addAll(configuration.getRoles());
        }
        this.usernameMap.put(username, new ArrayList<String>(usernameList));
        this.groupnameMap.put(username, new ArrayList<String>(groupnameList));
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getProfiles()
     */
    @Override
    public Profiles getProfiles() {
        return getConfiguration().getProfiles();
    }

    public RightsServerHandler getRightsServerHandler() {
        if (rightsServerHandler == null) {
            rightsServerHandler = new RightsServerHandler(this);
        }
        return rightsServerHandler;
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getMessage(java.lang.String)
     */
    @Override
    public String getMessage(String key) {
        String message;
        try {
            message = getMessages().getMessage(key, null, Locale.getDefault());
        } catch (Exception e) {
            log.warn("Message not found: " + key);
            if (log.isDebugEnabled()) {
                log.debug("Stacktrace: ", e);
            }
            message = key + " (!)";
        }
        return message;
    }

    /* (non-Javadoc)
     * @see sernet.verinice.interfaces.IRightsService#getAllMessages()
     */
    @Override
    public Properties getAllMessages() {
        return getMessages().getAllMessages();
    }

    /**
     * @return the authConfigurationDefault
     */
    public Resource getAuthConfigurationDefault() {
        return authConfigurationDefault;
    }

    /**
     * @param authConfigurationDefault the authConfigurationDefault to set
     */
    public void setAuthConfigurationDefault(Resource authConfigurationDefault) {
        this.authConfigurationDefault = authConfigurationDefault;
    }

    /**
     * @return the authConfiguration
     */
    public Resource getAuthConfiguration() {
        return authConfiguration;
    }

    /**
     * @param authConfiguration the authConfiguration to set
     */
    public void setAuthConfiguration(Resource authConfiguration) {
        this.authConfiguration = authConfiguration;
    }

    /**
     * @return the authConfigurationSchema
     */
    public Resource getAuthConfigurationSchema() {
        return authConfigurationSchema;
    }

    /**
     * @param authConfigurationSchema the authConfigurationSchema to set
     */
    public void setAuthConfigurationSchema(Resource authConfigurationSchema) {
        this.authConfigurationSchema = authConfigurationSchema;
    }

    /**
     * @return the configurationService
     */
    public IConfigurationService getConfigurationService() {
        return configurationService;
    }

    /**
     * @param configurationService the configurationService to set
     */
    public void setConfigurationService(IConfigurationService configurationService) {
        this.configurationService = configurationService;
    }

    /**
     * @return the configurationDao
     */
    public IBaseDao<Configuration, Integer> getConfigurationDao() {
        return configurationDao;
    }

    /**
     * @param configurationDao the configurationDao to set
     */
    public void setConfigurationDao(IBaseDao<Configuration, Integer> configurationDao) {
        this.configurationDao = configurationDao;
    }

    /**
     * @return the propertyDao
     */
    public IBaseDao<Property, Integer> getPropertyDao() {
        return propertyDao;
    }

    /**
     * @param propertyDao the propertyDao to set
     */
    public void setPropertyDao(IBaseDao<Property, Integer> propertyDao) {
        this.propertyDao = propertyDao;
    }

    /**
     * @return the messages
     */
    public IRemoteMessageSource getMessages() {
        return messages;
    }

    /**
     * @param messages the messages to set
     */
    public void setMessages(IRemoteMessageSource messages) {
        this.messages = messages;
    }

    /**
     * @return the context
     */
    private JAXBContext getContext() {
        if (context == null) {
            try {
                context = JAXBContext.newInstance(Auth.class);
            } catch (JAXBException e) {
                log.error("Error while creating JAXB context.", e);
            }
        }
        return context;
    }

    private Schema getSchema() {
        if (schema == null) {
            SchemaFactory sf = SchemaFactory.newInstance(javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI);
            try {
                schema = sf.newSchema(getAuthConfigurationSchema().getURL());
            } catch (Exception e) {
                log.error("Error while creating schema.", e);
            }
        }
        return schema;
    }

    public IAuthService getAuthService() {
        return authService;
    }

    public void setAuthService(IAuthService authService) {
        this.authService = authService;
    }

    private Map<String, Profile> getProfileMap() {
        if (profileMap == null) {
            Profiles profiles = getProfiles();
            profileMap = new HashMap<String, Profile>();
            for (Profile profile : profiles.getProfile()) {
                profileMap.put(profile.getName(), profile);
            }
        }
        return profileMap;
    }

    /**
     * Returns <code>true</code> if an action is
     * referenced by a user profile, false if not.
     * 
     * @param actionId The id of an action
     * @param auth An auth configuration
     * @return True if actionId is referenced in auth
     */
    private boolean isReferenced(String actionId, Auth auth) {
        boolean returnValue = false;
        try {
            Map<String, Action> actionMap = loadAllReferencedActions(auth);
            if (actionMap != null) {
                returnValue = actionMap.get(actionId) != null && isWhitelist()
                        || actionMap.get(actionId) == null && isBlacklist();
            }
            return returnValue;
        } catch (Exception e) {
            log.error("Error while checking action. Returning false", e);
            return returnValue;
        }
    }

    /**
     * Returns all actions in param <code>auth</code>
     * which are referenced by a user profile.
     * 
     * Actions are returned in a map. Key is the id of the action,
     * value is the action.
     * 
     * @param auth An auth configuration
     * @return All actions which are referenced by a user profile of an auth configuration
     */
    private Map<String, Action> loadAllReferencedActions(Auth auth) {
        Map<String, Action> actionMap = new HashMap<String, Action>();
        for (Userprofile userprofile : auth.getUserprofiles().getUserprofile()) {
            List<ProfileRef> profileList = userprofile.getProfileRef();
            if (profileList != null) {
                for (ProfileRef profileRef : profileList) {
                    Profile profileWithActions = getProfileMap().get(profileRef.getName());
                    if (profileWithActions != null) {
                        List<Action> actionList = profileWithActions.getAction();
                        for (Action action : actionList) {
                            actionMap.put(action.getId(), action);
                        }
                    } else {
                        log.error("Could not find profile " + profileRef.getName() + " of user "
                                + getAuthService().getUsername());
                    }
                }
            }
        }
        return actionMap;
    }

    private static List<IRightsChangeListener> getChangeListener() {
        return XmlRightsService.changeListener;
    }

    public static void addChangeListener(IRightsChangeListener listener) {
        getChangeListener().add(listener);
    }

    public static void removeChangeListener(IRightsChangeListener listener) {
        getChangeListener().remove(listener);
    }

    public boolean isWhitelist() {
        return ConfigurationType.WHITELIST.equals(getConfiguration().getType());
    }

    public boolean isBlacklist() {
        return ConfigurationType.BLACKLIST.equals(getConfiguration().getType());
    }
}