/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.ambari.server.security.authentication.kerberos;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.security.authorization.AmbariGrantedAuthority;
import org.apache.ambari.server.security.authorization.UserType;
import org.apache.ambari.server.security.authorization.Users;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.security.authentication.util.KerberosName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* AmbariAuthToLocalUserDetailsService is a {@link UserDetailsService} that translates
* a Kerberos principal name into a local username that may be used when looking up
* and Ambari user account.
*/
public class AmbariAuthToLocalUserDetailsService implements UserDetailsService {
private static final Logger LOG = LoggerFactory.getLogger(AmbariAuthToLocalUserDetailsService.class);
private final Users users;
private final List<UserType> userTypeOrder;
private final String authToLocalRules;
/**
* Constructor.
* <p>
* Given the Ambari {@link Configuration}, initializes the {@link KerberosName} class using
* the <code>auth-to-local</code> rules from {@link AmbariKerberosAuthenticationProperties#getAuthToLocalRules()}.
*
* @param configuration the Ambari configuration data
* @param users the Ambari users access object
* @throws AmbariException if an error occurs parsing the user-provided auth-to-local rules
*/
public AmbariAuthToLocalUserDetailsService(Configuration configuration, Users users) throws AmbariException {
String authToLocalRules = null;
List<UserType> orderedUserTypes = null;
if (configuration != null) {
AmbariKerberosAuthenticationProperties properties = configuration.getKerberosAuthenticationProperties();
if (properties != null) {
authToLocalRules = properties.getAuthToLocalRules();
orderedUserTypes = properties.getOrderedUserTypes();
}
}
if (StringUtils.isEmpty(authToLocalRules)) {
authToLocalRules = "DEFAULT";
}
if ((orderedUserTypes == null) || orderedUserTypes.isEmpty()) {
orderedUserTypes = Collections.singletonList(UserType.LDAP);
}
this.users = users;
this.userTypeOrder = orderedUserTypes;
this.authToLocalRules = authToLocalRules;
}
@Override
public UserDetails loadUserByUsername(String principal) throws UsernameNotFoundException {
try {
String username;
// Since KerberosName relies on a static variable to hold on to the auth-to-local rules, attempt
// to protect access to the rule set by blocking other threads from chaning the rules out from
// under us during this operation. Similar logic is used in org.apache.ambari.server.view.ViewContextImpl.getUsername().
synchronized (KerberosName.class) {
KerberosName.setRules(authToLocalRules);
username = new KerberosName(principal).getShortName();
}
if (username == null) {
String message = String.format("Failed to translate %s to a local username during Kerberos authentication.", principal);
LOG.warn(message);
throw new UsernameNotFoundException(message);
}
LOG.info("Translated {} to {} using auth-to-local rules during Kerberos authentication.", principal, username);
return createUser(username);
} catch (IOException e) {
String message = String.format("Failed to translate %s to a local username during Kerberos authentication: %s", principal, e.getLocalizedMessage());
LOG.warn(message);
throw new UsernameNotFoundException(message, e);
}
}
/**
* Given a username, finds an appropriate account in the Ambari database.
* <p>
* User accounts are searched in order of preferred user type as specified in the Ambari configuration
* ({@link Configuration#KERBEROS_AUTH_USER_TYPES}).
*
* @param username a username
* @return the user details of the found user, or <code>null</code> if an appropriate user was not found
*/
private UserDetails createUser(String username) {
// Iterate over the ordered user types... when an account for the username/type combination is
// found, build the related AmbariUserAuthentication instance and return it. Only the first
// match matters... this may be an issue and cause some ambiguity in the event multiple user
// types are specified in the configuration and multiple accounts for the same username, but
// different types (LOCAL vs LDAP, etc...).
for (UserType userType : userTypeOrder) {
org.apache.ambari.server.security.authorization.User user = users.getUser(username, userType);
if (user != null) {
Collection<AmbariGrantedAuthority> userAuthorities = users.getUserAuthorities(user.getUserName(), user.getUserType());
return new User(username, "", userAuthorities);
}
}
String message = String.format("Failed find user account for user with username of %s during Kerberos authentication.", username);
LOG.warn(message);
throw new UsernameNotFoundException(message);
}
}