eu.eubrazilcc.lvl.storage.oauth2.dao.PendingUserDAO.java Source code

Java tutorial

Introduction

Here is the source code for eu.eubrazilcc.lvl.storage.oauth2.dao.PendingUserDAO.java

Source

/*
 * Copyright 2014 EUBrazilCC (EU?Brazil Cloud Connect)
 * 
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by 
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * 
 *   http://ec.europa.eu/idabc/eupl
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and 
 * limitations under the Licence.
 * 
 * This product combines work with different licenses. See the "NOTICE" text
 * file for details on the various modules and licenses.
 * The "NOTICE" text file is part of the distribution. Any derivative works
 * that you distribute must include a readable copy of the "NOTICE" text file.
 */

package eu.eubrazilcc.lvl.storage.oauth2.dao;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Lists.transform;
import static eu.eubrazilcc.lvl.storage.mongodb.MongoDBConnector.MONGODB_CONN;
import static eu.eubrazilcc.lvl.storage.mongodb.MongoDBHelper.toProjection;
import static eu.eubrazilcc.lvl.storage.mongodb.jackson.MongoDBJsonMapper.JSON_MAPPER;
import static eu.eubrazilcc.lvl.storage.oauth2.PendingUser.copyOf;
import static eu.eubrazilcc.lvl.storage.security.shiro.CryptProvider.hashAndSaltPassword;
import static eu.eubrazilcc.lvl.storage.transform.UserTransientStore.startStore;
import static org.apache.commons.lang.StringUtils.isNotBlank;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

import javax.annotation.Nullable;

import org.apache.commons.lang.mutable.MutableLong;
import org.bson.types.ObjectId;
import org.slf4j.Logger;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.util.JSON;

import eu.eubrazilcc.lvl.core.Sorting;
import eu.eubrazilcc.lvl.core.geojson.Point;
import eu.eubrazilcc.lvl.core.geojson.Polygon;
import eu.eubrazilcc.lvl.storage.dao.BaseDAO;
import eu.eubrazilcc.lvl.storage.dao.WriteResult;
import eu.eubrazilcc.lvl.storage.mongodb.jackson.ObjectIdDeserializer;
import eu.eubrazilcc.lvl.storage.mongodb.jackson.ObjectIdSerializer;
import eu.eubrazilcc.lvl.storage.oauth2.PendingUser;
import eu.eubrazilcc.lvl.storage.transform.UserTransientStore;

/**
 * {@link PendingUser} DAO. This class detects any attempt to insert a new resource owner in the database with an unprotected password and 
 * automatically creates a salt and hashes the password before inserting the element in the database. Any element an empty salt will be considered
 * insecure and therefore the password will be protected. 
 * @author Erik Torres <ertorser@upv.es>
 */
public enum PendingUserDAO implements BaseDAO<String, PendingUser> {

    PENDING_USER_DAO;

    private final static Logger LOGGER = getLogger(PendingUserDAO.class);

    public static final String COLLECTION = "pending_users";
    public static final String PRIMARY_KEY = "pendingUser.pendingUserId";
    public static final String EMAIL_KEY = "pendingUser.user.email";

    private PendingUserDAO() {
        MONGODB_CONN.createIndex(PRIMARY_KEY, COLLECTION);
        MONGODB_CONN.createIndex(EMAIL_KEY, COLLECTION);
    }

    @Override
    public WriteResult<PendingUser> insert(final PendingUser pendingUser) {
        if (isNotBlank(pendingUser.getUser().getSalt())) {
            // remove transient fields from the element before saving it to the database
            final UserTransientStore<PendingUser> store = startStore(pendingUser);
            final DBObject obj = map(store);
            final String id = MONGODB_CONN.insert(obj, COLLECTION);
            // restore transient fields
            store.restore();
            return new WriteResult.Builder<PendingUser>().id(id).build();
        } else {
            // protect sensitive fields before storing the element in the database
            final PendingUser copy = copyOf(pendingUser);
            final String[] shadowed = hashAndSaltPassword(copy.getUser().getPassword());
            copy.getUser().setSalt(shadowed[0]);
            copy.getUser().setPassword(shadowed[1]);
            // remove transient fields from the element before saving it to the database
            final UserTransientStore<PendingUser> store = startStore(copy);
            final DBObject obj = map(store);
            final String id = MONGODB_CONN.insert(obj, COLLECTION);
            return new WriteResult.Builder<PendingUser>().id(id).element(copy).build();
        }
    }

    @Override
    public WriteResult<PendingUser> insert(final PendingUser pendingUser, final boolean ignoreDuplicates) {
        throw new UnsupportedOperationException(
                "Inserting ignoring duplicates is not currently supported in this class");
    }

    @Override
    public PendingUser update(final PendingUser pendingUser) {
        // remove transient fields from the element before saving it to the database
        final UserTransientStore<PendingUser> store = startStore(pendingUser);
        final DBObject obj = map(store);
        MONGODB_CONN.update(obj, key(pendingUser.getPendingUserId()), COLLECTION);
        // restore transient fields
        store.restore();
        return null;
    }

    @Override
    public void delete(final String pendingUserId) {
        MONGODB_CONN.remove(key(pendingUserId), COLLECTION);
    }

    @Override
    public List<PendingUser> findAll() {
        return list(0, Integer.MAX_VALUE, null, null, null, null);
    }

    @Override
    public PendingUser find(final String pendingUserId) {
        final BasicDBObject obj = MONGODB_CONN.get(key(pendingUserId), COLLECTION);
        return parseBasicDBObjectOrNull(obj);
    }

    @Override
    public List<PendingUser> list(final int start, final int size,
            final @Nullable ImmutableMap<String, String> filter, final @Nullable Sorting sorting,
            final @Nullable ImmutableMap<String, Boolean> projection, final @Nullable MutableLong count) {
        // execute the query in the database (unsupported filter)
        return transform(
                MONGODB_CONN.list(sortCriteria(), COLLECTION, start, size, null, toProjection(projection), count),
                new Function<BasicDBObject, PendingUser>() {
                    @Override
                    public PendingUser apply(final BasicDBObject obj) {
                        return parseBasicDBObject(obj);
                    }
                });
    }

    @Override
    public long count() {
        return MONGODB_CONN.count(COLLECTION);
    }

    @Override
    public List<PendingUser> getNear(final Point point, final double maxDistance) {
        throw new UnsupportedOperationException("Geospatial searches are not currently supported in this class");
    }

    @Override
    public List<PendingUser> geoWithin(final Polygon polygon) {
        throw new UnsupportedOperationException("Geospatial searches are not currently supported in this class");
    }

    @Override
    public void stats(final OutputStream os) throws IOException {
        MONGODB_CONN.stats(os, COLLECTION);
    }

    private BasicDBObject key(final String key) {
        return new BasicDBObject(PRIMARY_KEY, key);
    }

    private BasicDBObject emailKey(final String key) {
        return new BasicDBObject(EMAIL_KEY, key);
    }

    private BasicDBObject sortCriteria() {
        return new BasicDBObject(PRIMARY_KEY, 1);
    }

    private PendingUser parseBasicDBObject(final BasicDBObject obj) {
        final PendingUser pendingUser = map(obj).getPendingUser();
        return pendingUser;
    }

    private PendingUser parseBasicDBObjectOrNull(final BasicDBObject obj) {
        PendingUser pendingUser = null;
        if (obj != null) {
            final PendingUserEntity entity = map(obj);
            if (entity != null) {
                pendingUser = entity.getPendingUser();
            }
        }
        return pendingUser;
    }

    @SuppressWarnings("unused")
    private PendingUser parseObject(final Object obj) {
        final BasicDBObject obj2 = (BasicDBObject) obj;
        return map((BasicDBObject) obj2.get("obj")).getPendingUser();
    }

    private DBObject map(final UserTransientStore<PendingUser> store) {
        DBObject obj = null;
        try {
            obj = (DBObject) JSON.parse(JSON_MAPPER.writeValueAsString(new PendingUserEntity(store.purge())));
        } catch (JsonProcessingException e) {
            LOGGER.error("Failed to write pending user to DB object", e);
        }
        return obj;
    }

    private PendingUserEntity map(final BasicDBObject obj) {
        PendingUserEntity entity = null;
        try {
            entity = JSON_MAPPER.readValue(obj.toString(), PendingUserEntity.class);
        } catch (IOException e) {
            LOGGER.error("Failed to read pending user from DB object", e);
        }
        return entity;
    }

    public static void updatePassword(final PendingUser pendingUser, final String newPassword) {
        final String shadowed = hashAndSaltPassword(newPassword, pendingUser.getUser().getSalt());
        pendingUser.getUser().setPassword(shadowed);
    }

    /**
     * Search for a pending user in the database using the specified email address.
     * @param email - email address whose associate pending user is to be returned
     * @return the pending user to which the specified email address is associated in 
     *         the database, or {@code null} if the database contains no entry for the email
     */
    public PendingUser findByEmail(final String email) {
        final BasicDBObject obj = MONGODB_CONN.get(emailKey(email), COLLECTION);
        return parseBasicDBObjectOrNull(obj);
    }

    /**
     * Checks whether or not the specified user name and confirmation code coincides with 
     * those stored for pending user identified by the provided id.
     * @param pendingUserId - identifier of the pending user
     * @param username - user name to be validated, or email address if {@code useEmail}
     *        is set to true
     * @param confirmationCode - confirmation code to be validated
     * @param useEmail - use email address to search the database, instead of pending user Id
     * @return {@code true} only if the provided user name and confirmation code coincides
     *        with those stored for the pending user. Otherwise, returns {@code false}.
     */
    public boolean isValid(final String pendingUserId, final String username, final String confirmationCode,
            final boolean useEmail) {
        return !useEmail ? isValidUsingPendingUserId(pendingUserId, username, confirmationCode)
                : isValidUsingEmail(username, confirmationCode);
    }

    private boolean isValidUsingPendingUserId(final String pendingUserId, final String username,
            final String activationCode) {
        checkArgument(isNotBlank(pendingUserId), "Uninitialized or invalid pending user id");
        checkArgument(isNotBlank(username), "Uninitialized or invalid username");
        checkArgument(isNotBlank(activationCode), "Uninitialized or invalid activation code");
        final PendingUser pendingUser = find(pendingUserId);
        final boolean isValid = (pendingUser != null && pendingUser.getUser() != null
                && username.equals(pendingUser.getUser().getUserid())
                && activationCode.equals(pendingUser.getActivationCode()));
        return isValid;
    }

    private boolean isValidUsingEmail(final String email, final String activationCode) {
        checkArgument(isNotBlank(email), "Uninitialized or invalid email address");
        checkArgument(isNotBlank(activationCode), "Uninitialized or invalid activation code");
        final PendingUser pendingUser = findByEmail(email);
        final boolean isValid = (pendingUser != null && pendingUser.getUser() != null
                && email.equals(pendingUser.getUser().getEmail())
                && activationCode.equals(pendingUser.getActivationCode()));
        return isValid;
    }

    /**
     * Pending user entity.
     * @author Erik Torres <ertorser@upv.es>
     */
    public static class PendingUserEntity {

        @JsonSerialize(using = ObjectIdSerializer.class)
        @JsonDeserialize(using = ObjectIdDeserializer.class)
        @JsonProperty("_id")
        private ObjectId id;

        private PendingUser pendingUser;

        public PendingUserEntity() {
        }

        public PendingUserEntity(final PendingUser pendingUser) {
            setPendingUser(pendingUser);
        }

        public ObjectId getId() {
            return id;
        }

        public void setId(final ObjectId id) {
            this.id = id;
        }

        public PendingUser getPendingUser() {
            return pendingUser;
        }

        public void setPendingUser(final PendingUser pendingUser) {
            this.pendingUser = pendingUser;
        }

    }

}