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

org.openl.rules.webstudio.security.LdapToOpenLUserDetailsMapper Maven / Gradle / Ivy

There is a newer version: 5.27.9
Show newest version
package org.openl.rules.webstudio.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.naming.CompositeName;
import javax.naming.ConfigurationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.PropertyResolver;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.UserDetailsContextMapper;

import org.openl.rules.security.Privilege;
import org.openl.rules.security.SimplePrivilege;
import org.openl.rules.security.SimpleUser;
import org.openl.rules.security.User;
import org.openl.util.StringUtils;

public class LdapToOpenLUserDetailsMapper implements UserDetailsContextMapper {
    private final Logger log = LoggerFactory.getLogger(LdapToOpenLUserDetailsMapper.class);
    private final UserDetailsContextMapper delegate;
    private final Consumer syncUserData;
    private final BiFunction, Collection> privilegeMapper;

    private final String groupFilter;

    // Fields below are needed to make additional requests to AD:
    private final String domain;
    private final String url;
    private final String rootDn;
    private final String searchFilter;

    public LdapToOpenLUserDetailsMapper(UserDetailsContextMapper delegate,
                                        Consumer syncUserData,
                                        PropertyResolver propertyResolver,
                                        BiFunction, Collection> privilegeMapper) {
        this.delegate = delegate;
        this.syncUserData = syncUserData;
        this.privilegeMapper = privilegeMapper;
        String domainProperty = propertyResolver.getProperty("security.ad.domain");
        this.domain = StringUtils.isNotBlank(domainProperty) ? domainProperty.toLowerCase() : null;
        this.url = propertyResolver.getProperty("security.ad.server-url");
        this.searchFilter = propertyResolver.getProperty("security.ad.search-filter");
        this.groupFilter = propertyResolver.getProperty("security.ad.group-filter");

        rootDn = this.domain == null ? null : rootDnFromDomain(this.domain);
    }

    @Override
    public UserDetails mapUserFromContext(DirContextOperations ctx,
                                          String username,
                                          Collection authorities) {
        UserDetails userDetails = delegate.mapUserFromContext(ctx, username, authorities);

        String firstName = ctx.getStringAttribute("givenName");
        String lastName = ctx.getStringAttribute("sn");
        String email = ctx.getStringAttribute("mail");
        String displayName = ctx.getStringAttribute("displayName");
        if (StringUtils.isBlank(displayName)) {
            displayName = ctx.getStringAttribute("cn");
        }

        Collection userAuthorities = getAuthorities(ctx,
                username,
                userDetails.getAuthorities());

        String fixedUsername = fixCaseMatching(ctx, username);

        SimpleUser simpleUser = SimpleUser.builder()
                .setFirstName(firstName)
                .setLastName(lastName)
                .setUsername(fixedUsername)
                .setPrivileges(userAuthorities.stream().map(GrantedAuthority::getAuthority).map(SimplePrivilege::new).collect(Collectors.toList()))
                .setEmail(email)
                .setDisplayName(displayName)
                .build();

        syncUserData.accept(simpleUser);

        Collection privileges = privilegeMapper.apply(fixedUsername, userAuthorities);

        return SimpleUser.builder(simpleUser).setPrivileges(privileges).build();
    }

    private String fixCaseMatching(DirContextOperations ctx, String username) {
        String userPrincipalName = ctx.getStringAttribute("userPrincipalName");
        if (username.equalsIgnoreCase(userPrincipalName)) {
            return userPrincipalName;
        }
        String sAMAccountName = ctx.getStringAttribute("sAMAccountName");
        if (username.equalsIgnoreCase(sAMAccountName)) {
            return sAMAccountName;
        }
        String uid = ctx.getStringAttribute("uid");
        if (username.equalsIgnoreCase(uid)) {
            return uid;
        }
        String krbPrincipalName = ctx.getStringAttribute("krbPrincipalName");
        if (username.equalsIgnoreCase(krbPrincipalName)) {
            return krbPrincipalName;
        }
        return username.toLowerCase();
    }

    @Override
    public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
        delegate.mapUserToContext(user, ctx);
    }

    private Collection getAuthorities(DirContextOperations ctx,
                                                                  String username,
                                                                  Collection fallbackAuthorities) {
        Collection userAuthorities = null;

        Authentication authentication = AuthenticationHolder.getAuthentication();
        if (authentication != null && authentication.getCredentials() instanceof String) {
            // Try to load nested groups and primary group of a user
            userAuthorities = loadUserAuthorities(ctx, username, (String) authentication.getCredentials());
        }

        if (userAuthorities == null) {
            // Fallback to default implementation
            userAuthorities = fallbackAuthorities;
        }
        return userAuthorities;
    }

    /**
     * Load nested groups and primary group. If error is occurred return null.
     *
     * @return Not null list if successful and null if cannot load user authorities because of an error.
     */
    private Collection loadUserAuthorities(DirContextOperations userData,
                                                                       String username,
                                                                       String password) {
        try {
            String bindPrincipal = createBindPrincipal(username);
            String searchRoot = rootDn != null ? rootDn : searchRootFromPrincipal(bindPrincipal);

            DirContext context = bindAsUser(bindPrincipal, password);

            SearchControls searchControls = new SearchControls();
            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            DistinguishedName searchBaseDn = new DistinguishedName(searchRoot);

            // This search must be done using DirContext with java.naming.ldap.attributes.binary attribute is set to
            // "objectSid" because in current implementation of ActiveDirectoryLdapAuthenticationProvider the object
            // "userData"
            // contains objectSid attribute with String type and is broken.
            NamingEnumeration userSearch = context
                    .search(searchBaseDn, searchFilter, new Object[]{bindPrincipal, username}, searchControls);
            if (!userSearch.hasMoreElements()) {
                log.warn("Cannot find account '{}'. Skip nested groups and primary group search.", username);
                return null;
            }
            userSearch.close();

            // Find all groups
            NamingEnumeration groupsSearch = context.search(searchBaseDn,
                    groupFilter,
                    new Object[]{bindPrincipal, username, userData.getDn()},
                    searchControls);

            // Fill authorities using search result
            ArrayList authorities = new ArrayList<>();
            try {
                while (groupsSearch.hasMore()) {
                    SearchResult searchResult = groupsSearch.next();
                    DistinguishedName dn = new DistinguishedName(new CompositeName(searchResult.getName()));

                    if (!searchRoot.isEmpty()) {
                        dn.prepend(searchBaseDn);
                    }

                    authorities.add(new SimpleGrantedAuthority(dn.removeLast().getValue()));
                }
            } catch (PartialResultException e) {
                groupsSearch.close();
                log.info("Ignoring PartialResultException with message: {}", e.getMessage(), e);
            }

            return authorities;
        } catch (NamingException e) {
            log.error(e.getMessage(), e);
            return null;
        }
    }

    private DirContext bindAsUser(String bindPrincipal, String password) throws NamingException {
        Hashtable env = new Hashtable<>();
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, bindPrincipal);
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.SECURITY_CREDENTIALS, password);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.OBJECT_FACTORIES, DefaultDirObjectFactory.class.getName());
        // To handle "Unprocessed Continuation Reference(s)" errors
        env.put(Context.REFERRAL, "follow");

        return new InitialLdapContext(env, null);
    }

    private String searchRootFromPrincipal(String bindPrincipal) throws NamingException {
        int atChar = bindPrincipal.lastIndexOf('@');

        if (atChar < 0) {
            String message = "User principal '" + bindPrincipal + "' does not contain the domain, and no domain has been configured";
            log.error(message);
            throw new ConfigurationException(message);
        }

        return rootDnFromDomain(bindPrincipal.substring(atChar + 1));
    }

    private String rootDnFromDomain(String domain) {
        String[] tokens = org.springframework.util.StringUtils.tokenizeToStringArray(domain, ".");
        StringBuilder root = new StringBuilder();

        for (String token : tokens) {
            if (root.length() > 0) {
                root.append(',');
            }
            root.append("dc=").append(token);
        }

        return root.toString();
    }

    private String createBindPrincipal(String username) {
        if (domain == null || username.toLowerCase().endsWith(domain)) {
            return username;
        }

        return username + "@" + domain;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy