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

io.gravitee.am.gateway.handler.common.utils.RiskAssessmentService Maven / Gradle / Ivy

/**
 * 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.gateway.handler.common.utils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.gravitee.am.common.jwt.Claims;
import io.gravitee.am.gateway.handler.common.auth.AuthenticationDetails;
import io.gravitee.am.model.Device;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.User;
import io.gravitee.am.model.UserActivity;
import io.gravitee.am.service.DeviceService;
import io.gravitee.am.service.UserActivityService;
import io.gravitee.risk.assessment.api.assessment.AssessmentMessage;
import io.gravitee.risk.assessment.api.assessment.AssessmentMessageResult;
import io.gravitee.risk.assessment.api.assessment.AssessmentResult;
import io.gravitee.risk.assessment.api.assessment.data.AssessmentData;
import io.gravitee.risk.assessment.api.assessment.settings.AssessmentSettings;
import io.gravitee.risk.assessment.api.assessment.settings.RiskAssessmentSettings;
import io.gravitee.risk.assessment.api.devices.Devices;
import io.gravitee.risk.assessment.api.geovelocity.GeoTimeCoordinate;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.vertx.core.AsyncResult;
import io.vertx.rxjava3.core.Vertx;
import io.vertx.rxjava3.core.eventbus.EventBus;
import io.vertx.rxjava3.core.eventbus.Message;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.List;

import static io.gravitee.am.common.utils.ConstantKeys.DEVICE_ID;
import static io.gravitee.risk.assessment.api.assessment.Assessment.NONE;
import static java.util.Objects.nonNull;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

/**
 * @author Michael CARTER (michael.carter at graviteesource.com)
 * @author Rémi SULTAN (remi.sultan at graviteesource.com)
 * @author GraviteeSource Team
 */
@Component
public class RiskAssessmentService {

    private static final Logger logger = LoggerFactory.getLogger(RiskAssessmentService.class);
    private static final String RISK_ASSESSMENT_SERVICE = "service:risk-assessment";

    private final DeviceService deviceService;
    private final UserActivityService userActivityService;
    private final ObjectMapper objectMapper;
    private final EventBus eventBus;
    private final RiskAssessmentSettings riskAssessmentSettings;

    public RiskAssessmentService(
            DeviceService deviceService,
            UserActivityService userActivityService,
            ObjectMapper objectMapper,
            RiskAssessmentSettings riskAssessmentSettings,
            Vertx vertx) {
        this.deviceService = deviceService;
        this.userActivityService = userActivityService;
        this.objectMapper = objectMapper;
        this.riskAssessmentSettings = riskAssessmentSettings;
        this.eventBus = vertx.eventBus();
    }

    public Maybe computeRiskAssessment(
            AuthenticationDetails authenticationDetails) {
        if (riskAssessmentSettings.isEnabled()) {
            var assessmentMessage = Single.just(new AssessmentMessage().setSettings(riskAssessmentSettings).setData(new AssessmentData()));
            Domain domain = authenticationDetails.getDomain();
            User user = authenticationDetails.getUser();
            return assessmentMessage
                    .flatMap(buildDeviceAssessmentMessage(authenticationDetails, domain, user))
                    .flatMap(buildIpReputationMessage(authenticationDetails))
                    .flatMap(buildGeoVelocityMessage(domain, user))
                    .flatMapMaybe(this::consumeRiskAssessmentResult)
                    .doOnError(throwable -> logger.error("An unexpected error has occurred while trying to apply risk assessment: ", throwable));
        }
        return Maybe.empty();
    }

    private Function> buildDeviceAssessmentMessage(
            AuthenticationDetails authDetails,
            Domain domain,
            User user) {
        return assessmentMessage -> {
            var deviceAssessment = ofNullable(riskAssessmentSettings.getDeviceAssessment()).orElse(new AssessmentSettings());
            if (deviceAssessment.isEnabled()) {
                logger.debug("Decorating assessment with devices");
                return deviceService.findByDomainAndUser(domain.getId(), user.getId())
                        .map(Device::getDeviceId)
                        .toList().flatMap(deviceIds -> {
                            assessmentMessage.getData().setDevices(new Devices()
                                    .setKnownDevices(deviceIds)
                                    .setEvaluatedDevice((String) authDetails.getPrincipal().getContext().get(DEVICE_ID))
                            );
                            return Single.just(assessmentMessage);
                        });
            }
            return Single.just(assessmentMessage);
        };
    }

    private Function> buildIpReputationMessage(AuthenticationDetails authDetails) {
        return assessmentMessage -> {
            var ipReputationAssessment = ofNullable(riskAssessmentSettings.getIpReputationAssessment()).orElse(new AssessmentSettings());
            if (ipReputationAssessment.isEnabled()) {
                logger.debug("Decorating assessment with IP reputation");
                String ip = (String) authDetails.getPrincipal().getContext().get(Claims.ip_address);
                assessmentMessage.getData().setCurrentIp(ip);
            }
            return Single.just(assessmentMessage);
        };
    }

    private Function> buildGeoVelocityMessage(Domain domain, User user) {
        return assessmentMessage -> {
            var geoVelocityAssessment = ofNullable(riskAssessmentSettings.getGeoVelocityAssessment()).orElse(new AssessmentSettings());
            if (geoVelocityAssessment.isEnabled()) {
                return userActivityService.findByDomainAndTypeAndUserAndLimit(domain.getId(), UserActivity.Type.LOGIN, user.getId(), 2)
                        .toList()
                        .flatMap(activityList -> {
                            assessmentMessage.getData().setGeoTimeCoordinates(computeGeoTimeCoordinates(activityList));
                            return Single.just(assessmentMessage);
                        });
            }
            return Single.just(assessmentMessage);
        };
    }

    private List computeGeoTimeCoordinates(List activityList) {
        return activityList.stream().filter(ua -> nonNull(ua.getLatitude())).filter(ua -> nonNull(ua.getLongitude()))
                .map(ua -> new GeoTimeCoordinate(
                        ua.getLatitude(),
                        ua.getLongitude(),
                        ua.getCreatedAt().toInstant().getEpochSecond())
                ).collect(toList());
    }

    private Maybe consumeRiskAssessmentResult(AssessmentMessage message) throws JsonProcessingException {
        return eventBus.rxRequest(RISK_ASSESSMENT_SERVICE, objectMapper.writeValueAsString(message))
                .flatMapMaybe(stringMessage -> Maybe.just(extractMessageResult(stringMessage)))
                .onErrorResumeNext(throwable -> {
                    final Throwable cause = ofNullable(throwable.getCause()).orElse(throwable);
                    logger.warn("{} could not be called, reason: {}", RISK_ASSESSMENT_SERVICE, cause.getMessage());
                    logger.debug("", cause);
                    return Maybe.empty();
                });
    }

    private AssessmentMessageResult extractMessageResult(Message response) {
        try {
            return objectMapper.readValue(response.body(), AssessmentMessageResult.class);
        } catch (JsonProcessingException e) {
            logger.error("An unexpected error has occurred: ", e);
            return new AssessmentMessageResult()
                    .setDevices(new AssessmentResult().setAssessment(NONE))
                    .setGeoVelocity(new AssessmentResult().setAssessment(NONE))
                    .setIpReputation(new AssessmentResult().setAssessment(NONE));
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy