xref: /OpenGrok/plugins/src/main/java/opengrok/auth/plugin/LdapAttrPlugin.java (revision 7b43edc3e135be8ba18592d2f0676003f24ebf44)
1b28a5538SAdam Hornacek /*
2b28a5538SAdam Hornacek  * CDDL HEADER START
3b28a5538SAdam Hornacek  *
4b28a5538SAdam Hornacek  * The contents of this file are subject to the terms of the
5b28a5538SAdam Hornacek  * Common Development and Distribution License (the "License").
6b28a5538SAdam Hornacek  * You may not use this file except in compliance with the License.
7b28a5538SAdam Hornacek  *
8b28a5538SAdam Hornacek  * See LICENSE.txt included in this distribution for the specific
9b28a5538SAdam Hornacek  * language governing permissions and limitations under the License.
10b28a5538SAdam Hornacek  *
11b28a5538SAdam Hornacek  * When distributing Covered Code, include this CDDL HEADER in each
12b28a5538SAdam Hornacek  * file and include the License file at LICENSE.txt.
13b28a5538SAdam Hornacek  * If applicable, add the following below this CDDL HEADER, with the
14b28a5538SAdam Hornacek  * fields enclosed by brackets "[]" replaced with your own identifying
15b28a5538SAdam Hornacek  * information: Portions Copyright [yyyy] [name of copyright owner]
16b28a5538SAdam Hornacek  *
17b28a5538SAdam Hornacek  * CDDL HEADER END
18b28a5538SAdam Hornacek  */
19b28a5538SAdam Hornacek 
20b28a5538SAdam Hornacek /*
21*7b43edc3SVladimir Kotal  * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved.
22b28a5538SAdam Hornacek  */
23b28a5538SAdam Hornacek package opengrok.auth.plugin;
24b28a5538SAdam Hornacek 
25b28a5538SAdam Hornacek import java.io.IOException;
26b28a5538SAdam Hornacek import java.nio.file.Files;
27b28a5538SAdam Hornacek import java.nio.file.Paths;
28b28a5538SAdam Hornacek import java.util.Map;
29b28a5538SAdam Hornacek import java.util.Set;
30b28a5538SAdam Hornacek import java.util.TreeSet;
31b28a5538SAdam Hornacek import java.util.logging.Level;
32b28a5538SAdam Hornacek import java.util.logging.Logger;
33b28a5538SAdam Hornacek import java.util.stream.Stream;
34aa6abf42SAdam Hornacek 
35aa6abf42SAdam Hornacek import jakarta.servlet.http.HttpServletRequest;
36b28a5538SAdam Hornacek import opengrok.auth.entity.LdapUser;
37b28a5538SAdam Hornacek import opengrok.auth.plugin.entity.User;
3817deb9edSVladimir Kotal import opengrok.auth.plugin.ldap.AbstractLdapProvider;
39b28a5538SAdam Hornacek import opengrok.auth.plugin.ldap.LdapException;
40b28a5538SAdam Hornacek import org.opengrok.indexer.authorization.AuthorizationException;
41b28a5538SAdam Hornacek import org.opengrok.indexer.configuration.Group;
42b28a5538SAdam Hornacek import org.opengrok.indexer.configuration.Project;
43b28a5538SAdam Hornacek 
44b28a5538SAdam Hornacek /**
45b28a5538SAdam Hornacek  * Authorization plug-in to check user's LDAP attribute against whitelist.
46b28a5538SAdam Hornacek  *
4766cf937cSVladimir Kotal  * This plugin heavily relies on the presence of the {@code LdapUserPlugin} in the stack above it,
4866cf937cSVladimir Kotal  * since it is using the Distinguished Name of the {@code LdapUser} to perform the LDAP lookup.
4966cf937cSVladimir Kotal  *
50b28a5538SAdam Hornacek  * @author Krystof Tulinger
51b28a5538SAdam Hornacek  */
52b28a5538SAdam Hornacek public class LdapAttrPlugin extends AbstractLdapPlugin {
53b28a5538SAdam Hornacek 
54b28a5538SAdam Hornacek     private static final Logger LOGGER = Logger.getLogger(LdapAttrPlugin.class.getName());
55b28a5538SAdam Hornacek 
5666cf937cSVladimir Kotal     /**
5766cf937cSVladimir Kotal      * List of configuration names.
5866cf937cSVladimir Kotal      * <ul>
5966cf937cSVladimir Kotal      * <li><code>attribute</code> is LDAP attribute to check (mandatory)</li>
6066cf937cSVladimir Kotal      * <li><code>file</code> whitelist file (mandatory)</li>
6166cf937cSVladimir Kotal      * <li><code>instance</code> is number of <code>LdapUserInstance</code> plugin to use (optional)</li>
6266cf937cSVladimir Kotal      * </ul>
6366cf937cSVladimir Kotal      */
641fc76d71SVladimir Kotal     static final String ATTR_PARAM = "attribute";
651fc76d71SVladimir Kotal     static final String FILE_PARAM = "file";
661fc76d71SVladimir Kotal     static final String INSTANCE_PARAM = "instance";
671fc76d71SVladimir Kotal 
681fc76d71SVladimir Kotal     private static final String SESSION_ALLOWED_PREFIX = "opengrok-ldap-attr-plugin-allowed";
691fc76d71SVladimir Kotal     private String sessionAllowed = SESSION_ALLOWED_PREFIX;
701fc76d71SVladimir Kotal 
71b28a5538SAdam Hornacek     private String ldapAttr;
72b28a5538SAdam Hornacek     private final Set<String> whitelist = new TreeSet<>();
7366cf937cSVladimir Kotal     private Integer ldapUserInstance;
74cbd2aafbSVladimir Kotal     private String filePath;
75b28a5538SAdam Hornacek 
LdapAttrPlugin()76b28a5538SAdam Hornacek     public LdapAttrPlugin() {
77b28a5538SAdam Hornacek         sessionAllowed += "-" + nextId++;
78b28a5538SAdam Hornacek     }
79b28a5538SAdam Hornacek 
8041d15cd7SVladimir Kotal     // for testing
load(Map<String, Object> parameters, AbstractLdapProvider provider)8141d15cd7SVladimir Kotal     void load(Map<String, Object> parameters, AbstractLdapProvider provider) {
8241d15cd7SVladimir Kotal         super.load(provider);
8341d15cd7SVladimir Kotal 
8441d15cd7SVladimir Kotal         init(parameters);
8541d15cd7SVladimir Kotal     }
8641d15cd7SVladimir Kotal 
87b28a5538SAdam Hornacek     @Override
load(Map<String, Object> parameters)88b28a5538SAdam Hornacek     public void load(Map<String, Object> parameters) {
89b28a5538SAdam Hornacek         super.load(parameters);
90b28a5538SAdam Hornacek 
9141d15cd7SVladimir Kotal         init(parameters);
9241d15cd7SVladimir Kotal     }
9341d15cd7SVladimir Kotal 
init(Map<String, Object> parameters)9431639808SVladimir Kotal     private void init(Map<String, Object> parameters) {
95b28a5538SAdam Hornacek         if ((ldapAttr = (String) parameters.get(ATTR_PARAM)) == null) {
96b28a5538SAdam Hornacek             throw new NullPointerException("Missing param [" + ATTR_PARAM + "] in the setup");
97b28a5538SAdam Hornacek         }
98b28a5538SAdam Hornacek 
99b28a5538SAdam Hornacek         if ((filePath = (String) parameters.get(FILE_PARAM)) == null) {
100b28a5538SAdam Hornacek             throw new NullPointerException("Missing param [" + FILE_PARAM + "] in the setup");
101b28a5538SAdam Hornacek         }
102b28a5538SAdam Hornacek 
103f7f40d76SVladimir Kotal         String instance = (String) parameters.get(INSTANCE_PARAM);
10466cf937cSVladimir Kotal         if (instance != null) {
10566cf937cSVladimir Kotal             ldapUserInstance = Integer.parseInt(instance);
10666cf937cSVladimir Kotal         }
10766cf937cSVladimir Kotal 
108b28a5538SAdam Hornacek         try (Stream<String> stream = Files.lines(Paths.get(filePath))) {
109b28a5538SAdam Hornacek             stream.forEach(whitelist::add);
110b28a5538SAdam Hornacek         } catch (IOException e) {
111b28a5538SAdam Hornacek             throw new IllegalArgumentException(String.format("Unable to read the file \"%s\"", filePath), e);
112b28a5538SAdam Hornacek         }
11366cf937cSVladimir Kotal 
114dd7c760cSVladimir Kotal         LOGGER.log(Level.FINE, "LdapAttrPlugin plugin loaded with attr={0}, whitelist={1} ({2} entries), " +
115dd7c760cSVladimir Kotal                         "instance={3}", new Object[]{ldapAttr, filePath, whitelist.size(), ldapUserInstance});
116b28a5538SAdam Hornacek     }
117b28a5538SAdam Hornacek 
118b28a5538SAdam Hornacek     @Override
sessionExists(HttpServletRequest req)119b28a5538SAdam Hornacek     protected boolean sessionExists(HttpServletRequest req) {
120b28a5538SAdam Hornacek         return super.sessionExists(req)
121b28a5538SAdam Hornacek                 && req.getSession().getAttribute(sessionAllowed) != null;
122b28a5538SAdam Hornacek     }
123b28a5538SAdam Hornacek 
getSessionAllowedAttrName()124f7f40d76SVladimir Kotal     String getSessionAllowedAttrName() {
125f7f40d76SVladimir Kotal         return sessionAllowed;
126f7f40d76SVladimir Kotal     }
127f7f40d76SVladimir Kotal 
128b28a5538SAdam Hornacek     @Override
fillSession(HttpServletRequest req, User user)129b28a5538SAdam Hornacek     public void fillSession(HttpServletRequest req, User user) {
13017eebd12SVladimir Kotal         updateSession(req, false);
131b28a5538SAdam Hornacek 
132268d0236SVladimir Kotal         LdapUser ldapUser = (LdapUser) req.getSession().getAttribute(LdapUserPlugin.getSessionAttrName(ldapUserInstance));
133268d0236SVladimir Kotal         if (ldapUser == null) {
134e467a492SVladimir Kotal             LOGGER.log(Level.WARNING, "cannot get {0} attribute from {1}",
135e467a492SVladimir Kotal                     new Object[]{LdapUserPlugin.SESSION_ATTR, user});
136b28a5538SAdam Hornacek             return;
137b28a5538SAdam Hornacek         }
138b28a5538SAdam Hornacek 
139b28a5538SAdam Hornacek         // Check attributes cached in LDAP user object first, then query LDAP server
140b28a5538SAdam Hornacek         // (and if found, cache the result in the LDAP user object).
141e0086822SVladimir Kotal         Set<String> attributeValues = ldapUser.getAttribute(ldapAttr);
142cbd2aafbSVladimir Kotal         if (attributeValues == null) {
143e0086822SVladimir Kotal             Map<String, Set<String>> records = null;
144cbd2aafbSVladimir Kotal             AbstractLdapProvider ldapProvider = getLdapProvider();
145b28a5538SAdam Hornacek             try {
14602df4614SVladimir Kotal                 String dn = ldapUser.getDn();
14717deb9edSVladimir Kotal                 if (dn != null) {
148cbd2aafbSVladimir Kotal                     LOGGER.log(Level.FINEST, "searching with dn={0} on {1}",
149cbd2aafbSVladimir Kotal                             new Object[]{dn, ldapProvider});
15017deb9edSVladimir Kotal                     AbstractLdapProvider.LdapSearchResult<Map<String, Set<String>>> res;
151cbd2aafbSVladimir Kotal                     if ((res = ldapProvider.lookupLdapContent(dn, new String[]{ldapAttr})) == null) {
152cbd2aafbSVladimir Kotal                         LOGGER.log(Level.WARNING, "cannot lookup attributes {0} for user {1} on {2})",
153e467a492SVladimir Kotal                                 new Object[]{ldapAttr, ldapUser, ldapProvider});
1544126de0dSVladimir Kotal                         return;
15553c33ae5SVladimir Kotal                     }
15653c33ae5SVladimir Kotal 
15717deb9edSVladimir Kotal                     records = res.getAttrs();
15802df4614SVladimir Kotal                 } else {
159e310f75eSVladimir Kotal                     LOGGER.log(Level.FINE, "no DN for LDAP user {0} on {1}",
160e467a492SVladimir Kotal                             new Object[]{ldapUser, ldapProvider});
161b28a5538SAdam Hornacek                 }
162b28a5538SAdam Hornacek             } catch (LdapException ex) {
163b28a5538SAdam Hornacek                 throw new AuthorizationException(ex);
164b28a5538SAdam Hornacek             }
165b28a5538SAdam Hornacek 
16653c33ae5SVladimir Kotal             if (records == null || records.isEmpty() || (attributeValues = records.get(ldapAttr)) == null) {
167cbd2aafbSVladimir Kotal                 LOGGER.log(Level.WARNING, "empty records or attribute values {0} for user {1} on {2}",
168e467a492SVladimir Kotal                         new Object[]{ldapAttr, ldapUser, ldapProvider});
169b28a5538SAdam Hornacek                 return;
170b28a5538SAdam Hornacek             }
171b28a5538SAdam Hornacek 
172b28a5538SAdam Hornacek             ldapUser.setAttribute(ldapAttr, attributeValues);
173b28a5538SAdam Hornacek         }
174b28a5538SAdam Hornacek 
175*7b43edc3SVladimir Kotal         boolean isAttrInWhitelist = attributeValues.stream().anyMatch(whitelist::contains);
176cbd2aafbSVladimir Kotal         LOGGER.log(Level.FINEST, "LDAP user {0} {1} against {2}",
177*7b43edc3SVladimir Kotal                 new Object[]{ldapUser, isAttrInWhitelist ? "allowed" : "denied", filePath});
178*7b43edc3SVladimir Kotal         updateSession(req, isAttrInWhitelist);
179b28a5538SAdam Hornacek     }
180b28a5538SAdam Hornacek 
181b28a5538SAdam Hornacek     /**
182b28a5538SAdam Hornacek      * Add a new allowed value into the session.
183b28a5538SAdam Hornacek      *
184b28a5538SAdam Hornacek      * @param req the request
185b28a5538SAdam Hornacek      * @param allowed the new value
186b28a5538SAdam Hornacek      */
updateSession(HttpServletRequest req, boolean allowed)18731639808SVladimir Kotal     private void updateSession(HttpServletRequest req, boolean allowed) {
188b28a5538SAdam Hornacek         req.getSession().setAttribute(sessionAllowed, allowed);
189b28a5538SAdam Hornacek     }
190b28a5538SAdam Hornacek 
191b28a5538SAdam Hornacek     @Override
checkEntity(HttpServletRequest request, Project project)192b28a5538SAdam Hornacek     public boolean checkEntity(HttpServletRequest request, Project project) {
193b28a5538SAdam Hornacek         return ((Boolean) request.getSession().getAttribute(sessionAllowed));
194b28a5538SAdam Hornacek     }
195b28a5538SAdam Hornacek 
196b28a5538SAdam Hornacek     @Override
checkEntity(HttpServletRequest request, Group group)197b28a5538SAdam Hornacek     public boolean checkEntity(HttpServletRequest request, Group group) {
198b28a5538SAdam Hornacek         return ((Boolean) request.getSession().getAttribute(sessionAllowed));
199b28a5538SAdam Hornacek     }
200b28a5538SAdam Hornacek }
201