/* * Copyright 2012 John Crygier * Copyright 2012 gitblit.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit; import java.io.File; import java.text.MessageFormat; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.utils.ConnectionUtils.BlindSSLSocketFactory; import com.gitblit.utils.StringUtils; /** * Implementation of an LDAP user service. * * @author John Crygier */ public class LdapUserService extends GitblitUserService { public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class); private final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private IStoredSettings settings; public LdapUserService() { super(); } @Override public void setup(IStoredSettings settings) { this.settings = settings; String file = settings.getString(Keys.realm.ldap_backingUserService, "users.conf"); File realmFile = GitBlit.getFileOrFolder(file); serviceImpl = createUserService(realmFile); logger.info("LDAP User Service backed by " + serviceImpl.toString()); } /** * Credentials are defined in the LDAP server and can not be manipulated * from Gitblit. * * @return false * @since 1.0.0 */ @Override public boolean supportsCredentialChanges() { return false; } /** * If the LDAP server will maintain team memberships then LdapUserService * will not allow team membership changes. In this scenario all team * changes must be made on the LDAP server by the LDAP administrator. * * @return true or false * @since 1.0.0 */ public boolean supportsTeamMembershipChanges() { return !settings.getBoolean(Keys.realm.ldap_maintainTeams, false); } /** * Does the user service support cookie authentication? * * @return true or false */ @Override public boolean supportsCookies() { // TODO cookies need to be reviewed return false; } @Override public UserModel authenticate(String username, char[] password) { String domainUser = getDomainUsername(username); DirContext ctx = getDirContext(domainUser, new String(password)); // TODO do we need a bind here? if (ctx != null) { String simpleUsername = getSimpleUsername(username); UserModel user = getUserModel(simpleUsername); if (user == null) { // create user object for new authenticated user user = new UserModel(simpleUsername.toLowerCase()); } user.password = new String(password); if (!supportsTeamMembershipChanges()) { // Teams are specified in LDAP server // TODO search LDAP for team memberships Set foundTeams = new HashSet(); for (String team : foundTeams) { TeamModel model = getTeamModel(team); if (model == null) { // create the team model = new TeamModel(team.toLowerCase()); updateTeamModel(model); } // add team to the user user.teams.add(model); } } try { ctx.close(); } catch (NamingException e) { logger.error("Can not close context", e); } return user; } return null; } protected DirContext getDirContext() { String username = settings.getString(Keys.realm.ldap_username, ""); String password = settings.getString(Keys.realm.ldap_password, ""); return getDirContext(username, password); } protected DirContext getDirContext(String username, String password) { try { String server = settings.getRequiredString(Keys.realm.ldap_server); Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, CONTEXT_FACTORY); env.put(Context.PROVIDER_URL, server); if (server.startsWith("ldaps:")) { env.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName()); } // TODO consider making this a setting env.put("com.sun.jndi.ldap.read.timeout", "5000"); if (!StringUtils.isEmpty(username)) { // authenticated login env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, getDomainUsername(username)); env.put(Context.SECURITY_CREDENTIALS, password == null ? "":password.trim()); } return new InitialDirContext(env); } catch (NamingException e) { logger.warn(MessageFormat.format("Error connecting to LDAP with credentials. Please check {0}, {1}, and {2}", Keys.realm.ldap_server, Keys.realm.ldap_username, Keys.realm.ldap_password), e); return null; } } /** * Returns a simple username without any domain prefixes. * * @param username * @return a simple username */ protected String getSimpleUsername(String username) { int lastSlash = username.lastIndexOf('\\'); if (lastSlash > -1) { username = username.substring(lastSlash + 1); } return username; } /** * Returns a username with a domain prefix as long as the username does not * already have a comain prefix. * * @param username * @return a domain username */ protected String getDomainUsername(String username) { String domain = settings.getString(Keys.realm.ldap_domain, null); String domainUsername = username; if (!StringUtils.isEmpty(domain) && (domainUsername.indexOf('\\') == -1)) { domainUsername = domain + "\\" + username; } return domainUsername.trim(); } }