LCOV - code coverage report
Current view: top level - auth/ldap - LdapRealm.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 42 159 26.4 %
Date: 2022-11-19 15:00:39 Functions: 11 27 40.7 %

          Line data    Source code
       1             : // Copyright (C) 2009 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.auth.ldap;
      16             : 
      17             : import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
      18             : 
      19             : import com.google.common.base.Strings;
      20             : import com.google.common.cache.CacheLoader;
      21             : import com.google.common.cache.LoadingCache;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.gerrit.common.Nullable;
      24             : import com.google.gerrit.common.data.ParameterizedString;
      25             : import com.google.gerrit.entities.Account;
      26             : import com.google.gerrit.entities.AccountGroup;
      27             : import com.google.gerrit.entities.GroupReference;
      28             : import com.google.gerrit.extensions.client.AccountFieldName;
      29             : import com.google.gerrit.extensions.client.AuthType;
      30             : import com.google.gerrit.server.account.AbstractRealm;
      31             : import com.google.gerrit.server.account.AccountException;
      32             : import com.google.gerrit.server.account.AuthRequest;
      33             : import com.google.gerrit.server.account.EmailExpander;
      34             : import com.google.gerrit.server.account.GroupBackends;
      35             : import com.google.gerrit.server.account.externalids.ExternalId;
      36             : import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
      37             : import com.google.gerrit.server.account.externalids.ExternalIds;
      38             : import com.google.gerrit.server.auth.AuthenticationUnavailableException;
      39             : import com.google.gerrit.server.auth.NoSuchUserException;
      40             : import com.google.gerrit.server.config.AuthConfig;
      41             : import com.google.gerrit.server.config.GerritServerConfig;
      42             : import com.google.gerrit.server.logging.Metadata;
      43             : import com.google.gerrit.server.logging.TraceContext;
      44             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      45             : import com.google.inject.Inject;
      46             : import com.google.inject.Singleton;
      47             : import com.google.inject.name.Named;
      48             : import java.io.IOException;
      49             : import java.util.Arrays;
      50             : import java.util.Collection;
      51             : import java.util.HashMap;
      52             : import java.util.HashSet;
      53             : import java.util.List;
      54             : import java.util.Locale;
      55             : import java.util.Map;
      56             : import java.util.Optional;
      57             : import java.util.Set;
      58             : import java.util.concurrent.ExecutionException;
      59             : import javax.naming.CompositeName;
      60             : import javax.naming.Name;
      61             : import javax.naming.NamingException;
      62             : import javax.naming.directory.DirContext;
      63             : import javax.security.auth.login.LoginException;
      64             : import org.eclipse.jgit.lib.Config;
      65             : 
      66             : @Singleton
      67             : class LdapRealm extends AbstractRealm {
      68           2 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      69             : 
      70             :   static final String LDAP = "com.sun.jndi.ldap.LdapCtxFactory";
      71             :   static final String USERNAME = "username";
      72             : 
      73             :   private final Helper helper;
      74             :   private final AuthConfig authConfig;
      75             :   private final EmailExpander emailExpander;
      76             :   private final LoadingCache<String, Optional<Account.Id>> usernameCache;
      77             :   private final Set<AccountFieldName> readOnlyAccountFields;
      78             :   private final boolean fetchMemberOfEagerly;
      79             :   private final String mandatoryGroup;
      80             :   private final LdapGroupBackend groupBackend;
      81             : 
      82             :   private final Config config;
      83             : 
      84             :   private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
      85             : 
      86             :   @Inject
      87             :   LdapRealm(
      88             :       Helper helper,
      89             :       AuthConfig authConfig,
      90             :       EmailExpander emailExpander,
      91             :       LdapGroupBackend groupBackend,
      92             :       @Named(LdapModule.GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
      93             :       @Named(LdapModule.USERNAME_CACHE) LoadingCache<String, Optional<Account.Id>> usernameCache,
      94           2 :       @GerritServerConfig Config config) {
      95           2 :     this.helper = helper;
      96           2 :     this.authConfig = authConfig;
      97           2 :     this.emailExpander = emailExpander;
      98           2 :     this.groupBackend = groupBackend;
      99           2 :     this.usernameCache = usernameCache;
     100           2 :     this.membershipCache = membershipCache;
     101           2 :     this.config = config;
     102             : 
     103           2 :     this.readOnlyAccountFields = new HashSet<>();
     104             : 
     105           2 :     if (optdef(config, "accountFullName", "DEFAULT") != null) {
     106           2 :       readOnlyAccountFields.add(AccountFieldName.FULL_NAME);
     107             :     }
     108           2 :     if (optdef(config, "accountSshUserName", "DEFAULT") != null) {
     109           2 :       readOnlyAccountFields.add(AccountFieldName.USER_NAME);
     110             :     }
     111           2 :     if (!authConfig.isAllowRegisterNewEmail()) {
     112           0 :       readOnlyAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL);
     113             :     }
     114             : 
     115           2 :     fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true);
     116           2 :     mandatoryGroup = optional(config, "mandatoryGroup");
     117           2 :   }
     118             : 
     119             :   static SearchScope scope(Config c, String setting) {
     120           0 :     return c.getEnum("ldap", null, setting, SearchScope.SUBTREE);
     121             :   }
     122             : 
     123             :   static String optional(Config config, String name) {
     124           2 :     return config.getString("ldap", null, name);
     125             :   }
     126             : 
     127             :   static int optional(Config config, String name, int defaultValue) {
     128           0 :     return config.getInt("ldap", name, defaultValue);
     129             :   }
     130             : 
     131             :   static String optional(Config config, String name, String defaultValue) {
     132           2 :     final String v = optional(config, name);
     133           2 :     if (Strings.isNullOrEmpty(v)) {
     134           2 :       return defaultValue;
     135             :     }
     136           0 :     return v;
     137             :   }
     138             : 
     139             :   static boolean optional(Config config, String name, boolean defaultValue) {
     140           2 :     return config.getBoolean("ldap", name, defaultValue);
     141             :   }
     142             : 
     143             :   static String required(Config config, String name) {
     144           0 :     final String v = optional(config, name);
     145           0 :     if (v == null || "".equals(v)) {
     146           0 :       throw new IllegalArgumentException("No ldap." + name + " configured");
     147             :     }
     148           0 :     return v;
     149             :   }
     150             : 
     151             :   static List<String> optionalList(Config config, String name) {
     152           0 :     String[] s = config.getStringList("ldap", null, name);
     153           0 :     return Arrays.asList(s);
     154             :   }
     155             : 
     156             :   static List<String> requiredList(Config config, String name) {
     157           0 :     List<String> vlist = optionalList(config, name);
     158             : 
     159           0 :     if (vlist.isEmpty()) {
     160           0 :       throw new IllegalArgumentException("No ldap " + name + " configured");
     161             :     }
     162             : 
     163           0 :     return vlist;
     164             :   }
     165             : 
     166             :   @Nullable
     167             :   static String optdef(Config c, String n, String d) {
     168           2 :     final String[] v = c.getStringList("ldap", null, n);
     169           2 :     if (v == null || v.length == 0) {
     170           2 :       return d;
     171             : 
     172           0 :     } else if (v[0] == null || "".equals(v[0])) {
     173           0 :       return null;
     174             : 
     175             :     } else {
     176           0 :       checkBackendCompliance(n, v[0], Strings.isNullOrEmpty(d));
     177           0 :       return v[0];
     178             :     }
     179             :   }
     180             : 
     181             :   static String reqdef(Config c, String n, String d) {
     182           0 :     final String v = optdef(c, n, d);
     183           0 :     if (v == null) {
     184           0 :       throw new IllegalArgumentException("No ldap." + n + " configured");
     185             :     }
     186           0 :     return v;
     187             :   }
     188             : 
     189             :   @Nullable
     190             :   static ParameterizedString paramString(Config c, String n, String d) {
     191           0 :     String expression = optdef(c, n, d);
     192           0 :     if (expression == null) {
     193           0 :       return null;
     194           0 :     } else if (expression.contains("${")) {
     195           0 :       return new ParameterizedString(expression);
     196             :     } else {
     197           0 :       return new ParameterizedString("${" + expression + "}");
     198             :     }
     199             :   }
     200             : 
     201             :   private static void checkBackendCompliance(
     202             :       String configOption, String suppliedValue, boolean disabledByBackend) {
     203           0 :     if (disabledByBackend && !Strings.isNullOrEmpty(suppliedValue)) {
     204           0 :       String msg = String.format("LDAP backend doesn't support: ldap.%s", configOption);
     205           0 :       logger.atSevere().log("%s", msg);
     206           0 :       throw new IllegalArgumentException(msg);
     207             :     }
     208           0 :   }
     209             : 
     210             :   @Override
     211             :   public boolean allowsEdit(AccountFieldName field) {
     212           1 :     return !readOnlyAccountFields.contains(field);
     213             :   }
     214             : 
     215             :   @Nullable
     216             :   static String apply(ParameterizedString p, LdapQuery.Result m) throws NamingException {
     217           0 :     if (p == null) {
     218           0 :       return null;
     219             :     }
     220             : 
     221           0 :     final Map<String, String> values = new HashMap<>();
     222           0 :     for (String name : m.attributes()) {
     223           0 :       values.put(name, m.get(name));
     224           0 :     }
     225             : 
     226           0 :     String r = p.replace(values).trim();
     227           0 :     return r.isEmpty() ? null : r;
     228             :   }
     229             : 
     230             :   @Override
     231             :   public AuthRequest authenticate(AuthRequest who) throws AccountException {
     232           0 :     if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
     233           0 :       who.setLocalUser(who.getLocalUser().toLowerCase(Locale.US));
     234             :     }
     235             : 
     236           0 :     final String username = who.getLocalUser();
     237             :     try {
     238             :       final DirContext ctx;
     239           0 :       if (authConfig.getAuthType() == AuthType.LDAP_BIND) {
     240           0 :         ctx = helper.authenticate(username, who.getPassword());
     241             :       } else {
     242           0 :         ctx = helper.open();
     243             :       }
     244             :       try {
     245           0 :         final Helper.LdapSchema schema = helper.getSchema(ctx);
     246             :         LdapQuery.Result m;
     247           0 :         who.setAuthProvidesAccountActiveStatus(true);
     248           0 :         m = helper.findAccount(schema, ctx, username, fetchMemberOfEagerly);
     249           0 :         who.setActive(true);
     250             : 
     251           0 :         if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
     252             :           // We found the user account, but we need to verify
     253             :           // the password matches it before we can continue.
     254             :           //
     255           0 :           helper.close(helper.authenticate(m.getDN(), who.getPassword()));
     256             :         }
     257             : 
     258           0 :         who.setDisplayName(apply(schema.accountFullName, m));
     259           0 :         who.setUserName(apply(schema.accountSshUserName, m));
     260             : 
     261           0 :         if (schema.accountEmailAddress != null) {
     262           0 :           who.setEmailAddress(apply(schema.accountEmailAddress, m));
     263             : 
     264           0 :         } else if (emailExpander.canExpand(username)) {
     265             :           // If LDAP cannot give us a valid email address for this user
     266             :           // try expanding it through the older email expander code which
     267             :           // assumes a user name within a domain.
     268             :           //
     269           0 :           who.setEmailAddress(emailExpander.expand(username));
     270             :         }
     271             : 
     272             :         // Fill the cache with the user's current groups. We've already
     273             :         // spent the cost to open the LDAP connection, we might as well
     274             :         // do one more call to get their group membership. Since we are
     275             :         // in the middle of authenticating the user, its likely we will
     276             :         // need to know what access rights they have soon.
     277             :         //
     278           0 :         if (fetchMemberOfEagerly || mandatoryGroup != null) {
     279           0 :           Set<AccountGroup.UUID> groups = helper.queryForGroups(ctx, username, m);
     280           0 :           if (mandatoryGroup != null) {
     281           0 :             GroupReference mandatoryGroupRef =
     282           0 :                 GroupBackends.findExactSuggestion(groupBackend, mandatoryGroup);
     283           0 :             if (mandatoryGroupRef == null) {
     284           0 :               throw new AccountException("Could not identify mandatory group: " + mandatoryGroup);
     285             :             }
     286           0 :             if (!groups.contains(mandatoryGroupRef.getUUID())) {
     287           0 :               throw new AccountException(
     288           0 :                   "Not member of mandatory LDAP group: " + mandatoryGroupRef.getName());
     289             :             }
     290             :           }
     291             :           // Regardless if we enabled fetchMemberOfEagerly, we already have the
     292             :           // groups and it would be a waste not to cache them.
     293           0 :           membershipCache.put(username, groups);
     294             :         }
     295           0 :         return who;
     296             :       } finally {
     297           0 :         helper.close(ctx);
     298             :       }
     299           0 :     } catch (IOException | NamingException e) {
     300           0 :       logger.atSevere().withCause(e).log("Cannot query LDAP to authenticate user");
     301           0 :       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     302           0 :     } catch (LoginException e) {
     303           0 :       logger.atSevere().withCause(e).log("Cannot authenticate server via JAAS");
     304           0 :       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     305             :     }
     306             :   }
     307             : 
     308             :   @Override
     309             :   public void onCreateAccount(AuthRequest who, Account account) {
     310           0 :     usernameCache.put(who.getLocalUser(), Optional.of(account.id()));
     311           0 :   }
     312             : 
     313             :   @Nullable
     314             :   @Override
     315             :   public Account.Id lookup(String accountName) {
     316           0 :     if (Strings.isNullOrEmpty(accountName)) {
     317           0 :       return null;
     318             :     }
     319             :     try {
     320           0 :       Optional<Account.Id> id = usernameCache.get(accountName);
     321           0 :       return id != null ? id.orElse(null) : null;
     322           0 :     } catch (ExecutionException e) {
     323           0 :       logger.atWarning().withCause(e).log("Cannot lookup account %s in LDAP", accountName);
     324           0 :       return null;
     325             :     }
     326             :   }
     327             : 
     328             :   @Override
     329             :   public boolean isActive(String username)
     330             :       throws LoginException, NamingException, AccountException, IOException {
     331           0 :     final DirContext ctx = helper.open();
     332             :     try {
     333           0 :       Helper.LdapSchema schema = helper.getSchema(ctx);
     334           0 :       helper.findAccount(schema, ctx, username, false);
     335           0 :       return true;
     336           0 :     } catch (NoSuchUserException e) {
     337           0 :       return false;
     338             :     } finally {
     339           0 :       helper.close(ctx);
     340             :     }
     341             :   }
     342             : 
     343             :   @Override
     344             :   public boolean accountBelongsToRealm(Collection<ExternalId> externalIds) {
     345           2 :     for (ExternalId id : externalIds) {
     346           2 :       if (id.isScheme(SCHEME_GERRIT)) {
     347           2 :         return true;
     348             :       }
     349           2 :     }
     350           2 :     return false;
     351             :   }
     352             : 
     353             :   static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
     354             :     private final ExternalIds externalIds;
     355             :     private final ExternalIdKeyFactory externalIdKeyFactory;
     356             : 
     357             :     @Inject
     358           2 :     UserLoader(ExternalIds externalIds, ExternalIdKeyFactory externalIdKeyFactory) {
     359           2 :       this.externalIds = externalIds;
     360           2 :       this.externalIdKeyFactory = externalIdKeyFactory;
     361           2 :     }
     362             : 
     363             :     @Override
     364             :     public Optional<Account.Id> load(String username) throws Exception {
     365           0 :       try (TraceTimer timer =
     366           0 :           TraceContext.newTimer(
     367           0 :               "Loading account for username", Metadata.builder().username(username).build())) {
     368           0 :         return externalIds
     369           0 :             .get(externalIdKeyFactory.create(SCHEME_GERRIT, username))
     370           0 :             .map(ExternalId::accountId);
     371             :       }
     372             :     }
     373             :   }
     374             : 
     375             :   static class MemberLoader extends CacheLoader<String, Set<AccountGroup.UUID>> {
     376             :     private final Helper helper;
     377             : 
     378             :     @Inject
     379           2 :     MemberLoader(Helper helper) {
     380           2 :       this.helper = helper;
     381           2 :     }
     382             : 
     383             :     @Override
     384             :     public Set<AccountGroup.UUID> load(String username) throws Exception {
     385           0 :       try (TraceTimer timer =
     386           0 :           TraceContext.newTimer(
     387             :               "Loading group for member with username",
     388           0 :               Metadata.builder().username(username).build())) {
     389           0 :         final DirContext ctx = helper.open();
     390             :         try {
     391           0 :           return helper.queryForGroups(ctx, username, null);
     392             :         } finally {
     393           0 :           helper.close(ctx);
     394             :         }
     395             :       }
     396             :     }
     397             :   }
     398             : 
     399             :   static class ExistenceLoader extends CacheLoader<String, Boolean> {
     400             :     private final Helper helper;
     401             : 
     402             :     @Inject
     403           2 :     ExistenceLoader(Helper helper) {
     404           2 :       this.helper = helper;
     405           2 :     }
     406             : 
     407             :     @Override
     408             :     public Boolean load(String groupDn) throws Exception {
     409           0 :       try (TraceTimer timer =
     410           0 :           TraceContext.newTimer(
     411           0 :               "Loading groupDn", Metadata.builder().authDomainName(groupDn).build())) {
     412           0 :         final DirContext ctx = helper.open();
     413             :         try {
     414           0 :           Name compositeGroupName = new CompositeName().add(groupDn);
     415             :           try {
     416           0 :             ctx.getAttributes(compositeGroupName);
     417           0 :             return true;
     418           0 :           } catch (NamingException e) {
     419           0 :             return false;
     420             :           }
     421             :         } finally {
     422           0 :           helper.close(ctx);
     423             :         }
     424           0 :       }
     425             :     }
     426             :   }
     427             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750