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

de.fraunhofer.iosb.ilt.frostserver.mqtt.moquette.AuthWrapper Maven / Gradle / Ivy

/*
 * Copyright (C) 2024 Fraunhofer Institut IOSB, Fraunhoferstr. 1, D 76131
 * Karlsruhe, Germany.
 *
 * 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 .
 */
package de.fraunhofer.iosb.ilt.frostserver.mqtt.moquette;

import static de.fraunhofer.iosb.ilt.frostserver.mqtt.moquette.MoquetteMqttServer.TAG_MQTT_TOPIC_ALLOWLIST;
import static de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings.TAG_AUTH_ALLOW_ANON_READ;
import static de.fraunhofer.iosb.ilt.frostserver.util.StringHelper.isNullOrEmpty;

import de.fraunhofer.iosb.ilt.frostserver.mqtt.MqttManager;
import de.fraunhofer.iosb.ilt.frostserver.mqtt.subscription.SubscriptionFactory;
import de.fraunhofer.iosb.ilt.frostserver.parser.path.PathParser;
import de.fraunhofer.iosb.ilt.frostserver.path.ResourcePath;
import de.fraunhofer.iosb.ilt.frostserver.path.Version;
import de.fraunhofer.iosb.ilt.frostserver.persistence.PersistenceManager;
import de.fraunhofer.iosb.ilt.frostserver.persistence.PersistenceManagerFactory;
import de.fraunhofer.iosb.ilt.frostserver.service.InitResult;
import de.fraunhofer.iosb.ilt.frostserver.settings.CoreSettings;
import de.fraunhofer.iosb.ilt.frostserver.settings.Settings;
import de.fraunhofer.iosb.ilt.frostserver.settings.UnknownVersionException;
import de.fraunhofer.iosb.ilt.frostserver.util.AuthProvider;
import de.fraunhofer.iosb.ilt.frostserver.util.AuthUtils;
import de.fraunhofer.iosb.ilt.frostserver.util.StringHelper;
import de.fraunhofer.iosb.ilt.frostserver.util.exception.UpgradeFailedException;
import de.fraunhofer.iosb.ilt.frostserver.util.user.PrincipalExtended;
import io.moquette.broker.security.IAuthenticator;
import io.moquette.broker.security.IAuthorizatorPolicy;
import io.moquette.broker.subscriptions.Topic;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.net.URLDecoder;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Wraps the Moquette Auth classes and forwards the auth requests to the FROST
 * AuthProvider.
 */
public class AuthWrapper implements IAuthenticator, IAuthorizatorPolicy {

    /**
     * The logger for this class.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthWrapper.class);
    private static final AuthProvider AUTH_PROVIDER_DENY_ALL = new AuthProvider() {
        @Override
        public void addFilter(Object context, CoreSettings coreSettings) {
            // This dummy does not add filters.
        }

        @Override
        public boolean isValidUser(String clientId, String userName, String password) {
            return false;
        }

        @Override
        public boolean userHasRole(String clientId, String userName, String roleName) {
            return false;
        }

        @Override
        public InitResult init(CoreSettings coreSettings) {
            // This dummy needs no initialisation.
            return InitResult.INIT_OK;
        }

        @Override
        public String checkForUpgrades() {
            return "Something went wrong while configuring your auth provider. You have a configuration error.";
        }

        @Override
        public boolean doUpgrades(Writer out) throws UpgradeFailedException, IOException {
            return false;
        }

        @Override
        public PrincipalExtended getUserPrincipal(String clientId) {
            return PrincipalExtended.ANONYMOUS_PRINCIPAL;
        }

    };

    private final CoreSettings coreSettings;
    private final AuthProvider authProvider;
    private final boolean anonymousRead;
    private final String roleAdmin;
    private final String roleCeate;
    private final String roleRead;
    private final String frostClientId;
    private final Pattern topicAllowPattern;
    private PersistenceManager persistenceManager;

    public AuthWrapper(CoreSettings coreSettings, String authProviderClassName, String frostClientId) {
        LOGGER.info("Initialising authentication.");
        this.coreSettings = coreSettings;
        this.frostClientId = frostClientId;
        Settings authSettings = coreSettings.getAuthSettings();
        anonymousRead = authSettings.getBoolean(TAG_AUTH_ALLOW_ANON_READ, CoreSettings.class);
        String topicAllowListRegex = authSettings.get(TAG_MQTT_TOPIC_ALLOWLIST, MoquetteMqttServer.class);

        if (isNullOrEmpty(topicAllowListRegex)) {
            topicAllowPattern = null;
        } else {
            topicAllowPattern = Pattern.compile(topicAllowListRegex);
            PersistenceManagerFactory.init(coreSettings);
        }

        Map roleMapping = AuthUtils.loadRoleMapping(authSettings);
        roleAdmin = roleMapping.get(AuthUtils.Role.ADMIN);
        roleCeate = roleMapping.get(AuthUtils.Role.CREATE);
        roleRead = roleMapping.get(AuthUtils.Role.READ);

        AuthProvider tempAuthProvider;

        try {
            Class authConfigClass = ClassUtils.getClass(authProviderClassName);
            if (AuthProvider.class.isAssignableFrom(authConfigClass)) {
                Class filterConfigClass = (Class) authConfigClass;
                tempAuthProvider = filterConfigClass.getDeclaredConstructor().newInstance();
                tempAuthProvider.init(coreSettings);
            } else {
                LOGGER.error("Configured class does not implement AuthProvider: {}", authProviderClassName);
                tempAuthProvider = AUTH_PROVIDER_DENY_ALL;
            }
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException exc) {
            LOGGER.error("Could not initialise auth class.", exc);
            tempAuthProvider = AUTH_PROVIDER_DENY_ALL;
        }
        authProvider = tempAuthProvider;
    }

    @Override
    public boolean checkValid(String clientId, String username, byte[] password) {
        if (frostClientId.equalsIgnoreCase(clientId)) {
            LOGGER.error("FROST-ClientID Used in checkValid");
            return true;
        }
        return authProvider.isValidUser(clientId, username, new String(password, StringHelper.UTF8));
    }

    @Override
    public boolean canWrite(Topic topic, String user, String clientId) {
        if (frostClientId.equalsIgnoreCase(clientId)) {
            LOGGER.error("FROST-ClientID Used in write");
            return true;
        }
        return authProvider.userHasRole(clientId, user, roleCeate);
    }

    @Override
    public boolean canRead(Topic mqttTopic, String user, String clientId) {
        String topic = mqttTopic.toString();
        if (frostClientId.equalsIgnoreCase(clientId)) {
            LOGGER.error("FROST-ClientID Used in read");
            return true;
        }
        if (StringUtils.containsAny(topic, '#', '+')) {
            LOGGER.debug("Denied access to {}, wildcards not allowed.", topic);
            return false;
        }
        PrincipalExtended userPrincipal = authProvider.getUserPrincipal(clientId);
        if (user != null && !user.equals(userPrincipal.getName())) {
            LOGGER.warn("Username {} does not match name in Principal: {}", user, userPrincipal);
            return false;
        }
        if (user == null && userPrincipal != PrincipalExtended.ANONYMOUS_PRINCIPAL) {
            LOGGER.warn("Username is null, but Principal is: {}", userPrincipal);
            return false;
        }
        if (userPrincipal.hasRole(roleRead) || userPrincipal.hasRole(roleAdmin)) {
            LOGGER.debug("Allowing access to {}, user {} has role 'read' or 'admin'.", topic, user);
            return true;
        }
        if (topicAllowPattern != null) {
            Version version;
            try {
                version = MqttManager.getVersionFromTopic(coreSettings, topic);
            } catch (UnknownVersionException ex) {
                LOGGER.debug("Denied access to {}, unknown version.", topic);
                return false;
            }
            String internalTopic = topic.substring(version.urlPart.length() + 1);
            internalTopic = SubscriptionFactory.getPathFromTopic(internalTopic);

            if (!topicAllowPattern.matcher(internalTopic).matches()) {
                LOGGER.debug("Denied access to {}, not matching allow pattern.", internalTopic);
                return false;
            }

            return validatePath(version, internalTopic, userPrincipal);
        }

        return anonymousRead;
    }

    private boolean validatePath(Version version, String topic, PrincipalExtended userPrincipal) {
        PrincipalExtended lp = PrincipalExtended.getLocalPrincipal();
        try {
            String internalTopic = URLDecoder.decode(topic, StringHelper.UTF8.name());
            ResourcePath path = PathParser.parsePath(
                    coreSettings.getModelRegistry(),
                    coreSettings.getQueryDefaults().getServiceRootUrl(),
                    version,
                    internalTopic,
                    userPrincipal);
            PrincipalExtended.setLocalPrincipal(userPrincipal);
            boolean validPath = getPm().validatePath(path);
            if (validPath) {
                LOGGER.debug("Allowing access for user {} to {}.", topic, userPrincipal);
            } else {
                LOGGER.debug(" Denying access for user {} to {}.", topic, userPrincipal);
            }
            return validPath;
        } catch (RuntimeException | UnsupportedEncodingException ex) {
            LOGGER.warn("Exception trying to validate access for user {} to topic {}.", userPrincipal, topic, ex);
            return false;
        } finally {
            PrincipalExtended.setLocalPrincipal(lp);
        }
    }

    public PersistenceManager getPm() {
        if (persistenceManager == null) {
            persistenceManager = PersistenceManagerFactory.getInstance(coreSettings).create();
        }
        return persistenceManager;
    }

    public PrincipalExtended getUserPrincipal(String clientId) {
        return authProvider.getUserPrincipal(clientId);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy