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

io.gravitee.am.identityprovider.mongo.user.MongoUserProvider Maven / Gradle / Ivy

There is a newer version: 4.6.0-alpha.2
Show newest version
/**
 * Copyright (C) 2015 The Gravitee team (http://gravitee.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.gravitee.am.identityprovider.mongo.user;

import com.google.common.base.Strings;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Updates;
import com.mongodb.reactivestreams.client.MongoCollection;
import io.gravitee.am.common.oidc.StandardClaims;
import io.gravitee.am.common.utils.RandomString;
import io.gravitee.am.identityprovider.api.DefaultUser;
import io.gravitee.am.identityprovider.api.User;
import io.gravitee.am.identityprovider.api.UserProvider;
import io.gravitee.am.identityprovider.api.encoding.BinaryToTextEncoder;
import io.gravitee.am.identityprovider.mongo.MongoAbstractProvider;
import io.gravitee.am.identityprovider.mongo.authentication.spring.MongoAuthenticationProviderConfiguration;
import io.gravitee.am.service.exception.UserAlreadyExistsException;
import io.gravitee.am.service.exception.UserNotFoundException;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import org.bson.BsonDocument;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Import;

import java.security.SecureRandom;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import static com.mongodb.client.model.Filters.eq;

/**
 * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com)
 * @author GraviteeSource Team
 */
@Import({MongoAuthenticationProviderConfiguration.class})
public class MongoUserProvider extends MongoAbstractProvider implements UserProvider {
    private static final Logger LOGGER = LoggerFactory.getLogger(MongoUserProvider.class);
    private static final String FIELD_ID = "_id";
    private static final String FIELD_CREATED_AT = "createdAt";
    private static final String FIELD_UPDATED_AT = "updatedAt";
    private static final String INDEX_USERNAME = "username_1";
    private static final String INDEX_USERNAME_UNIQUE = "u1_unique";

    @Autowired
    private BinaryToTextEncoder binaryToTextEncoder;

    private MongoCollection usersCollection;

    @Value("${management.mongodb.ensureIndexOnStart:true}")
    private boolean ensureIndexOnStart;

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        // init users collection
        usersCollection = this.mongoClient.getDatabase(this.configuration.getDatabase()).getCollection(this.configuration.getUsersCollection());

        createOrUpdateIndex();
    }

    public UserProvider stop() throws Exception {
        if (this.clientWrapper != null) {
            this.clientWrapper.releaseClient();
        }
        return this;
    }

    @Override
    public Maybe findByEmail(String email) {
        String rawQuery = this.configuration.getFindUserByEmailQuery().replaceAll("\\?", email);
        String jsonQuery = convertToJsonString(rawQuery);
        BsonDocument query = BsonDocument.parse(jsonQuery);
        return Observable.fromPublisher(usersCollection.find(query).first()).firstElement().map(this::convert);
    }

    @Override
    public Maybe findByUsername(String username) {
        final String encodedUsername = getSafeUsername(username);

        String rawQuery = this.configuration.getFindUserByUsernameQuery();
        String jsonQuery = convertToJsonString(rawQuery).replaceAll("\\?", encodedUsername);
        BsonDocument query = BsonDocument.parse(jsonQuery);
        return Observable.fromPublisher(usersCollection.find(query).first()).firstElement().map(this::convert);
    }

    @Override
    public Single create(User user) {
        final String username = getSafeUsername(user.getUsername());

        return findByUsername(username)
                .isEmpty()
                .flatMap(isEmpty -> {
                    if (!isEmpty) {
                        return Single.error(new UserAlreadyExistsException(user.getUsername()));
                    } else {
                        Document document = new Document();
                        // set technical id
                        document.put(FIELD_ID, user.getId() != null ? user.getId() : RandomString.generate());
                        // set username
                        document.put(configuration.getUsernameField(), username);
                        // set password
                        if (user.getCredentials() != null) {
                            if (configuration.isUseDedicatedSalt()) {
                                byte[] salt = createSalt();
                                document.put(configuration.getPasswordField(), passwordEncoder.encode(user.getCredentials(), salt));
                                document.put(configuration.getPasswordSaltAttribute(), binaryToTextEncoder.encode(salt));
                            } else {
                                document.put(configuration.getPasswordField(), passwordEncoder.encode(user.getCredentials()));
                            }
                        }
                        // set additional information
                        if (user.getAdditionalInformation() != null) {
                            document.putAll(user.getAdditionalInformation());
                        }
                        // set date fields
                        document.put(FIELD_CREATED_AT, new Date());
                        document.put(FIELD_UPDATED_AT, document.get(FIELD_CREATED_AT));
                        return Single.fromPublisher(usersCollection.insertOne(document)).flatMap(success -> findById(document.getString(FIELD_ID)).toSingle());
                    }
                });
    }

    @Override
    public Single update(String id, User updateUser) {
        return findById(id, false)
                .switchIfEmpty(Single.error(new UserNotFoundException(id)))
                .flatMap(oldUser -> {
                    Document document = new Document();
                    // set username (keep the original value)
                    document.put(configuration.getUsernameField(), oldUser.getUsername());
                    // set password
                    if (updateUser.getCredentials() != null) {
                        if (configuration.isUseDedicatedSalt()) {
                            byte[] salt = createSalt();
                            document.put(configuration.getPasswordField(), passwordEncoder.encode(updateUser.getCredentials(), salt));
                            document.put(configuration.getPasswordSaltAttribute(), binaryToTextEncoder.encode(salt));
                        } else {
                            document.put(configuration.getPasswordField(), passwordEncoder.encode(updateUser.getCredentials()));
                        }
                    } else {
                        document.put(configuration.getPasswordField(), oldUser.getCredentials());
                        if (configuration.isUseDedicatedSalt() && oldUser.getAdditionalInformation() != null) {
                            // be sure to include the current salt as we are using the MongoDB replaceOne method
                            document.put(configuration.getPasswordSaltAttribute(), oldUser.getAdditionalInformation().get(configuration.getPasswordSaltAttribute()));
                        }
                    }
                    // set additional information
                    if (updateUser.getAdditionalInformation() != null) {
                        document.putAll(updateUser.getAdditionalInformation());
                    }
                    // set date fields
                    document.put(FIELD_CREATED_AT, oldUser.getCreatedAt());
                    document.put(FIELD_UPDATED_AT, new Date());
                    return Single.fromPublisher(usersCollection.replaceOne(eq(FIELD_ID, oldUser.getId()), document)).flatMap(updateResult -> findById(oldUser.getId()).toSingle());
                });
    }

    @Override
    public Single updateUsername(User user, String username) {
        if (Strings.isNullOrEmpty(username)) {
            return Single.error(new IllegalArgumentException("Username required for UserProvider.updateUsername"));
        }

        return findById(user.getId())
                .switchIfEmpty(Single.error(new UserNotFoundException(user.getId())))
                .flatMap(foundUser -> {
                    var updates = Updates.combine(
                            Updates.set(configuration.getUsernameField(), username.toLowerCase()),
                            Updates.set(FIELD_UPDATED_AT, new Date()));
                    return Single.fromPublisher(usersCollection.updateOne(eq(FIELD_ID, user.getId()), updates))
                            .flatMap(updateResult -> findById(user.getId()).toSingle());
                });
    }

    @Override
    public Single updatePassword(User user, String password) {

        if (Strings.isNullOrEmpty(password)) {
            return Single.error(new IllegalArgumentException("Password required for UserProvider.updatePassword"));
        }

        return findById(user.getId(), false)
                .switchIfEmpty(Single.error(new UserNotFoundException(user.getId())))
                .flatMap(oldUser -> {
                    // set password
                    Bson passwordField = null;
                    Bson passwordSaltField = null;
                    if (configuration.isUseDedicatedSalt()) {
                        byte[] salt = createSalt();
                        passwordField = Updates.set(configuration.getPasswordField(), passwordEncoder.encode(password, salt));
                        passwordSaltField = Updates.set(configuration.getPasswordSaltAttribute(), binaryToTextEncoder.encode(salt));
                    } else {
                        passwordField = Updates.set(configuration.getPasswordField(), passwordEncoder.encode(password));
                    }

                    Bson updates = passwordSaltField == null ?
                            Updates.combine(
                                    passwordField,
                                    Updates.set(FIELD_UPDATED_AT, new Date())) :
                            Updates.combine(
                                    passwordField,
                                    passwordSaltField,
                                    Updates.set(FIELD_UPDATED_AT, new Date()));

                    return Single.fromPublisher(usersCollection.updateOne(eq(FIELD_ID, oldUser.getId()), updates))
                            .flatMap(updateResult -> findById(oldUser.getId()).toSingle());
                });
    }

    @Override
    public Completable delete(String id) {
        return findById(id)
                .switchIfEmpty(Maybe.error(new UserNotFoundException(id)))
                .flatMapCompletable(idpUser -> Completable.fromPublisher(usersCollection.deleteOne(eq(FIELD_ID, id))));
    }

    private Maybe findById(String userId) {
        return this.findById(userId, true);
    }

    private Maybe findById(String userId, boolean filterSalt) {
        return Observable.fromPublisher(usersCollection.find(eq(FIELD_ID, userId)).first()).firstElement().map(u -> convert(u, filterSalt));
    }

    private User convert(Document document) {
        return this.convert(document, true);
    }

    private User convert(Document document, boolean filterSalt) {
        String username = document.getString(configuration.getUsernameField());
        DefaultUser user = new DefaultUser(username);
        user.setId(document.getString(FIELD_ID));
        user.setCredentials(document.getString(configuration.getPasswordField()));
        user.setCreatedAt(document.get(FIELD_CREATED_AT, Date.class));
        user.setUpdatedAt(document.get(FIELD_UPDATED_AT, Date.class));
        // additional claims
        Map claims = new HashMap<>();
        claims.put(StandardClaims.SUB, document.getString(FIELD_ID));
        claims.put(StandardClaims.PREFERRED_USERNAME, username);
        // remove reserved claims
        document.remove(FIELD_ID);
        document.remove(configuration.getUsernameField());
        document.remove(configuration.getPasswordField());
        claims.putAll(document);

        if (filterSalt && configuration.isUseDedicatedSalt()) {
            claims.remove(configuration.getPasswordSaltAttribute());
        }

        user.setAdditionalInformation(claims);
        return user;
    }

    private String convertToJsonString(String rawString) {
        return rawString
                .replaceAll("[^" + JSON_SPECIAL_CHARS + "\\s]+", "\"$0\"")
                .replaceAll("\\s+", "");
    }

    private byte[] createSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[configuration.getPasswordSaltLength()];
        random.nextBytes(salt);
        return salt;
    }

    /**
     * Create or update index on username field
     */
    private void createOrUpdateIndex() {
        if (ensureIndexOnStart) {
            getUserNameIndex()
                    .andThen(Completable.fromPublisher(usersCollection.createIndex(new Document(configuration.getUsernameField(), 1),
                            new IndexOptions().name(INDEX_USERNAME_UNIQUE).unique(true))))
                    .subscribe(
                            () -> LOGGER.debug("Unique index on username created"),
                            err -> LOGGER.error("An error occurred while creating index {} with unique constraints", INDEX_USERNAME_UNIQUE, err));
        }
    }

    private Completable getUserNameIndex() {
        return Observable.fromPublisher(usersCollection.listIndexes())
                .map(document -> document.getString("name"))
                .flatMapCompletable(indexName -> {
                    if (indexName.equals(INDEX_USERNAME)) {
                        return Completable.fromPublisher(usersCollection.dropIndex(indexName));
                    } else {
                        return Completable.complete();
                    }
                });
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy