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

org.graylog.security.authservice.backend.ADAuthServiceBackend Maven / Gradle / Ivy

There is a newer version: 6.1.4
Show newest version
/*
 * Copyright (C) 2020 Graylog, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Server Side Public License, version 1,
 * as published by MongoDB, Inc.
 *
 * 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
 * Server Side Public License for more details.
 *
 * You should have received a copy of the Server Side Public License
 * along with this program. If not, see
 * .
 */
package org.graylog.security.authservice.backend;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.assistedinject.Assisted;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import org.graylog.security.authservice.AuthServiceBackend;
import org.graylog.security.authservice.AuthServiceBackendDTO;
import org.graylog.security.authservice.AuthServiceCredentials;
import org.graylog.security.authservice.AuthenticationDetails;
import org.graylog.security.authservice.ProvisionerService;
import org.graylog.security.authservice.UserDetails;
import org.graylog.security.authservice.ldap.LDAPConnectorConfig;
import org.graylog.security.authservice.ldap.LDAPUser;
import org.graylog.security.authservice.ldap.UnboundLDAPConfig;
import org.graylog.security.authservice.ldap.UnboundLDAPConnector;
import org.graylog.security.authservice.test.AuthServiceBackendTestResult;
import org.graylog2.security.encryption.EncryptedValue;
import org.graylog2.shared.security.AuthenticationServiceUnavailableException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class ADAuthServiceBackend implements AuthServiceBackend {
    public static final String TYPE_NAME = "active-directory";

    private static final Logger LOG = LoggerFactory.getLogger(ADAuthServiceBackend.class);

    public static final String AD_OBJECT_GUID = "objectGUID";
    public static final String AD_SAM_ACCOUNT_NAME = "sAMAccountName";
    public static final String AD_USER_PRINCIPAL_NAME = "userPrincipalName";
    public static final String AD_DISPLAY_NAME = "displayName";
    public static final String AD_CN = "cn";

    // Try both to avoid the need to configure the userSearchPattern setting
    public static final Filter AD_DEFAULT_USER_SEARCH_PATTERN = Filter.createANDFilter(
            Filter.createEqualityFilter("objectClass", "user"),
            Filter.createORFilter(
                    Filter.createEqualityFilter(AD_USER_PRINCIPAL_NAME, "{0}"),
                    Filter.createEqualityFilter(AD_SAM_ACCOUNT_NAME, "{0}")
            ));

    public interface Factory extends AuthServiceBackend.Factory {
        @Override
        ADAuthServiceBackend create(AuthServiceBackendDTO backend);
    }

    private final UnboundLDAPConnector ldapConnector;
    private final AuthServiceBackendDTO backend;
    private final ADAuthServiceBackendConfig config;

    @Inject
    public ADAuthServiceBackend(UnboundLDAPConnector ldapConnector,
                                @Assisted AuthServiceBackendDTO backend) {
        this.ldapConnector = ldapConnector;
        this.backend = backend;
        this.config = (ADAuthServiceBackendConfig) backend.config();
    }

    @Override
    public Optional authenticateAndProvision(AuthServiceCredentials authCredentials, ProvisionerService provisionerService) {
        try (final LDAPConnection connection = ldapConnector.connect(config.getLDAPConnectorConfig())) {
            if (connection == null) {
                return Optional.empty();
            }

            final Optional optionalUser = findUser(connection, authCredentials);
            if (!optionalUser.isPresent()) {
                LOG.debug("User <{}> not found in Active Directory", authCredentials.username());
                return Optional.empty();
            }

            final LDAPUser userEntry = optionalUser.get();

            if (!userEntry.accountIsEnabled()) {
                LOG.warn("Account disabled within Active Directory for user <{}> (DN: {})", authCredentials.username(), userEntry.dn());
                return Optional.empty();
            }
            if (!authCredentials.isAuthenticated()) {
                if (!isAuthenticated(connection, userEntry, authCredentials)) {
                    LOG.debug("Invalid credentials for user <{}> (DN: {})", authCredentials.username(), userEntry.dn());
                    return Optional.empty();
                }
            }

            final UserDetails userDetails = provisionerService.provision(provisionerService.newDetails(this)
                    .authServiceType(backendType())
                    .authServiceId(backendId())
                    .base64AuthServiceUid(userEntry.base64UniqueId())
                    .username(userEntry.username())
                    .accountIsEnabled(userEntry.accountIsEnabled())
                    .fullName(userEntry.fullName())
                    .email(userEntry.email())
                    .defaultRoles(backend.defaultRoles())
                    .build());

            return Optional.of(AuthenticationDetails.builder().userDetails(userDetails).build());
        } catch (GeneralSecurityException e) {
            LOG.error("Error setting up TLS connection", e);
            throw new AuthenticationServiceUnavailableException("Error setting up TLS connection", e);
        } catch (LDAPException e) {
            LOG.error("ActiveDirectory error", e);
            throw new AuthenticationServiceUnavailableException("ActiveDirectory error", e);
        }
    }

    private boolean isAuthenticated(LDAPConnection connection,
                                    LDAPUser user,
                                    AuthServiceCredentials authCredentials) throws LDAPException {
        return ldapConnector.authenticate(
                connection,
                // We need to bind against AD using the userPrincipalName
                user.entry().nonBlankAttribute(AD_USER_PRINCIPAL_NAME),
                authCredentials.password()
        );
    }

    private Optional findUser(LDAPConnection connection, AuthServiceCredentials authCredentials) throws LDAPException {
        final UnboundLDAPConfig searchConfig = UnboundLDAPConfig.builder()
                .userSearchBase(config.userSearchBase())
                .userSearchPattern(config.userSearchPattern())
                .userUniqueIdAttribute(AD_OBJECT_GUID)
                .userNameAttribute(config.userNameAttribute())
                .userFullNameAttribute(config.userFullNameAttribute())
                .emailAttributes(Arrays.asList("mail"))
                .build();

        return ldapConnector.searchUserByPrincipal(connection, searchConfig, authCredentials.username());
    }

    @Override
    public String backendType() {
        return TYPE_NAME;
    }

    @Override
    public String backendId() {
        return backend.id();
    }

    @Override
    public String backendTitle() {
        return backend.title();
    }

    @Override
    public AuthServiceBackendDTO prepareConfigUpdate(AuthServiceBackendDTO existingBackend, AuthServiceBackendDTO newBackend) {
        final ADAuthServiceBackendConfig newConfig = (ADAuthServiceBackendConfig) newBackend.config();

        if (newConfig.systemUserPassword().isDeleteValue()) {
            // If the system user password should be deleted, use an unset value
            return newBackend.toBuilder()
                    .config(newConfig.toBuilder()
                            .systemUserPassword(EncryptedValue.createUnset())
                            .build())
                    .build();
        }
        if (newConfig.systemUserPassword().isKeepValue()) {
            // If the system user password should be kept, use the value from the existing config
            final ADAuthServiceBackendConfig existingConfig = (ADAuthServiceBackendConfig) existingBackend.config();
            return newBackend.toBuilder()
                    .config(newConfig.toBuilder()
                            .systemUserPassword(existingConfig.systemUserPassword())
                            .build())
                    .build();
        }

        return newBackend;
    }

    @Override
    public AuthServiceBackendTestResult testConnection(@Nullable AuthServiceBackendDTO existingBackendConfig) {
        final ADAuthServiceBackendConfig testConfig = buildTestConfig(existingBackendConfig);

        final LDAPConnectorConfig config = testConfig.getLDAPConnectorConfig();

        if (config.serverList().size() == 1) {
            return testSingleConnection(config, config.serverList().get(0));
        }

        // Test each server separately, so we can see the result for each
        final List testResults = config.serverList().stream().map(server -> testSingleConnection(config, server)).collect(Collectors.toList());

        if (testResults.stream().anyMatch(res -> !res.isSuccess())) {
            return AuthServiceBackendTestResult
                    .createFailure("Test failure",
                            testResults.stream().map(r -> {
                                if (r.isSuccess()) {
                                    return r.message();
                                } else {
                                    return r.message() + " : " + String.join(",", r.errors());
                                }
                            }).collect(Collectors.toList()));
        } else {
            return AuthServiceBackendTestResult.createSuccess("Successfully connected to " + config.serverList());
        }
    }

    private AuthServiceBackendTestResult testSingleConnection(LDAPConnectorConfig config, LDAPConnectorConfig.LDAPServer server) {
        final LDAPConnectorConfig singleServerConfig = config.toBuilder().serverList(ImmutableList.of(server)).build();

        try (final LDAPConnection connection = ldapConnector.connect(singleServerConfig)) {
            if (connection == null) {
                return AuthServiceBackendTestResult.createFailure("Couldn't establish connection to " + server);
            }
            return AuthServiceBackendTestResult.createSuccess("Successfully connected to " + server);
        } catch (Exception e) {
            return AuthServiceBackendTestResult.createFailure(
                    "Couldn't establish connection to " + server,
                    Collections.singletonList(e.getMessage())
            );
        }
    }

    @Override
    public AuthServiceBackendTestResult testLogin(AuthServiceCredentials credentials, @Nullable AuthServiceBackendDTO existingBackendConfig) {
        final ADAuthServiceBackendConfig testConfig = buildTestConfig(existingBackendConfig);

        try (final LDAPConnection connection = ldapConnector.connect(testConfig.getLDAPConnectorConfig())) {
            if (connection == null) {
                return AuthServiceBackendTestResult.createFailure("Couldn't establish connection to " + testConfig.servers());
            }
            final Optional user = findUser(connection, credentials);

            if (!user.isPresent()) {
                return AuthServiceBackendTestResult.createFailure(
                        "User <" + credentials.username() + "> doesn't exist",
                        createTestResult(testConfig, false, false, null)
                );
            }

            if (isAuthenticated(connection, user.get(), credentials)) {
                return AuthServiceBackendTestResult.createSuccess(
                        "Successfully logged in <" + credentials.username() + "> into " + testConfig.servers(),
                        createTestResult(testConfig, true, true, user.get())
                );
            }
            return AuthServiceBackendTestResult.createFailure(
                    "Login for user <" + credentials.username() + "> failed",
                    createTestResult(testConfig, true, false, user.get())
            );
        } catch (Exception e) {
            return AuthServiceBackendTestResult.createFailure(
                    "Couldn't test user login on " + testConfig.servers(),
                    Collections.singletonList(e.getMessage())
            );
        }
    }

    private ADAuthServiceBackendConfig buildTestConfig(@Nullable AuthServiceBackendDTO existingBackendConfig) {
        final ADAuthServiceBackendConfig.Builder newConfigBuilder = config.toBuilder();

        // If the existing password should be kept and we got an existing config, use the password of the
        // existing config for the connection check. This is needed to make connection tests of existing backends work
        // because the UI doesn't have access to the existing password.
        if (config.systemUserPassword().isKeepValue() && existingBackendConfig != null) {
            final ADAuthServiceBackendConfig existingConfig = (ADAuthServiceBackendConfig) existingBackendConfig.config();
            newConfigBuilder.systemUserPassword(existingConfig.systemUserPassword());
        }

        return newConfigBuilder.build();
    }

    private Map createTestResult(ADAuthServiceBackendConfig config,
                                                 boolean userExists,
                                                 boolean loginSuccess,
                                                 @Nullable LDAPUser user) {
        final ImmutableMap.Builder userDetails = ImmutableMap.builder()
                .put("user_exists", userExists)
                .put("login_success", loginSuccess);

        if (user != null) {
            // Use regular HashMap to allow duplicates. Users might use the same attribute for name and full name.
            // See: https://github.com/Graylog2/graylog2-server/issues/10069
            final Map attributes = new HashMap<>();

            attributes.put("dn", user.dn());
            attributes.put("account_enabled", String.valueOf(user.accountIsEnabled()));
            // Use a special key for the unique ID attribute. If users use something like "uid" for the unique ID,
            // it might be confusing to see a base64 encoded value instead of the plain text one.
            attributes.put("unique_id (" + AD_OBJECT_GUID + ")", user.base64UniqueId());
            attributes.put(config.userNameAttribute(), user.username());
            attributes.put(config.userFullNameAttribute(), user.fullName());
            attributes.put("email", user.email());

            userDetails.put("user_details", ImmutableMap.copyOf(attributes));
        } else {
            userDetails.put("user_details", ImmutableMap.of());
        }

        return userDetails.build();
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy