| /* |
| * 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.cassandra.auth; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.Map; |
| import javax.security.auth.Subject; |
| import javax.security.auth.callback.*; |
| import javax.security.auth.login.FailedLoginException; |
| import javax.security.auth.login.LoginException; |
| import javax.security.auth.spi.LoginModule; |
| |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import org.apache.cassandra.config.DatabaseDescriptor; |
| import org.apache.cassandra.exceptions.AuthenticationException; |
| import org.apache.cassandra.service.StorageService; |
| |
| /** |
| * LoginModule which authenticates a user towards the Cassandra database using |
| * the internal authentication mechanism. |
| */ |
| public class CassandraLoginModule implements LoginModule |
| { |
| private static final Logger logger = LoggerFactory.getLogger(CassandraLoginModule.class); |
| |
| // initial state |
| private Subject subject; |
| private CallbackHandler callbackHandler; |
| |
| // the authentication status |
| private boolean succeeded = false; |
| private boolean commitSucceeded = false; |
| |
| // username and password |
| private String username; |
| private char[] password; |
| |
| private CassandraPrincipal principal; |
| |
| /** |
| * Initialize this {@code}LoginModule{@code}. |
| * |
| * @param subject the {@code}Subject{@code} to be authenticated. <p> |
| * @param callbackHandler a {@code}CallbackHandler{@code} for communicating |
| * with the end user (prompting for user names and passwords, for example) |
| * @param sharedState shared {@code}LoginModule{@code} state. This param is unused. |
| * @param options options specified in the login {@code}Configuration{@code} for this particular |
| * {@code}LoginModule{@code}. This param is unused |
| */ |
| @Override |
| public void initialize(Subject subject, |
| CallbackHandler callbackHandler, |
| Map<java.lang.String, ?> sharedState, |
| Map<java.lang.String, ?> options) |
| { |
| this.subject = subject; |
| this.callbackHandler = callbackHandler; |
| } |
| |
| /** |
| * Authenticate the user, obtaining credentials from the CallbackHandler |
| * supplied in {@code}initialize{@code}. As long as the configured |
| * {@code}IAuthenticator{@code} supports the optional |
| * {@code}legacyAuthenticate{@code} method, it can be used here. |
| * |
| * @return true in all cases since this {@code}LoginModule{@code} |
| * should not be ignored. |
| * @exception FailedLoginException if the authentication fails. |
| * @exception LoginException if this {@code}LoginModule{@code} is unable to |
| * perform the authentication. |
| */ |
| @Override |
| public boolean login() throws LoginException |
| { |
| // prompt for a user name and password |
| if (callbackHandler == null) |
| { |
| logger.info("No CallbackHandler available for authentication"); |
| throw new LoginException("Authentication failed"); |
| } |
| |
| NameCallback nc = new NameCallback("username: "); |
| PasswordCallback pc = new PasswordCallback("password: ", false); |
| try |
| { |
| callbackHandler.handle(new Callback[]{nc, pc}); |
| username = nc.getName(); |
| char[] tmpPassword = pc.getPassword(); |
| if (tmpPassword == null) |
| tmpPassword = new char[0]; |
| password = new char[tmpPassword.length]; |
| System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length); |
| pc.clearPassword(); |
| } |
| catch (IOException | UnsupportedCallbackException e) |
| { |
| logger.info("Unexpected exception processing authentication callbacks", e); |
| throw new LoginException("Authentication failed"); |
| } |
| |
| // verify the credentials |
| try |
| { |
| authenticate(); |
| } |
| catch (AuthenticationException e) |
| { |
| // authentication failed -- clean up |
| succeeded = false; |
| cleanUpInternalState(); |
| throw new FailedLoginException(e.getMessage()); |
| } |
| |
| succeeded = true; |
| return true; |
| } |
| |
| private void authenticate() |
| { |
| if (!StorageService.instance.isAuthSetupComplete()) |
| throw new AuthenticationException("Cannot login as server authentication setup is not yet completed"); |
| |
| IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator(); |
| Map<String, String> credentials = new HashMap<>(); |
| credentials.put(PasswordAuthenticator.USERNAME_KEY, username); |
| credentials.put(PasswordAuthenticator.PASSWORD_KEY, String.valueOf(password)); |
| AuthenticatedUser user = authenticator.legacyAuthenticate(credentials); |
| // Only actual users should be allowed to authenticate for JMX |
| if (user.isAnonymous() || user.isSystem()) |
| throw new AuthenticationException(String.format("Invalid user %s", user.getName())); |
| |
| // The LOGIN privilege is required to authenticate - c.f. ClientState::login |
| if (!DatabaseDescriptor.getRoleManager().canLogin(user.getPrimaryRole())) |
| throw new AuthenticationException(user.getName() + " is not permitted to log in"); |
| } |
| |
| /** |
| * This method is called if the LoginContext's overall authentication succeeded |
| * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules |
| * succeeded). |
| * |
| * If this LoginModule's own authentication attempt succeeded (checked by |
| * retrieving the private state saved by the {@code}login{@code} method), |
| * then this method associates a {@code}CassandraPrincipal{@code} |
| * with the {@code}Subject{@code}. |
| * If this LoginModule's own authentication attempted failed, then this |
| * method removes any state that was originally saved. |
| * |
| * @return true if this LoginModule's own login and commit attempts succeeded, false otherwise. |
| * @exception LoginException if the commit fails. |
| */ |
| @Override |
| public boolean commit() throws LoginException |
| { |
| if (!succeeded) |
| { |
| return false; |
| } |
| else |
| { |
| // add a Principal (authenticated identity) |
| // to the Subject |
| principal = new CassandraPrincipal(username); |
| if (!subject.getPrincipals().contains(principal)) |
| subject.getPrincipals().add(principal); |
| |
| cleanUpInternalState(); |
| commitSucceeded = true; |
| return true; |
| } |
| } |
| |
| /** |
| * This method is called if the LoginContext's overall authentication failed. |
| * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules |
| * did not succeed). |
| * |
| * If this LoginModule's own authentication attempt succeeded (checked by |
| * retrieving the private state saved by the {@code}login{@code} and |
| * {@code}commit{@code} methods), then this method cleans up any state that |
| * was originally saved. |
| * |
| * @return false if this LoginModule's own login and/or commit attempts failed, true otherwise. |
| * @throws LoginException if the abort fails. |
| */ |
| @Override |
| public boolean abort() throws LoginException |
| { |
| if (!succeeded) |
| { |
| return false; |
| } |
| else if (!commitSucceeded) |
| { |
| // login succeeded but overall authentication failed |
| succeeded = false; |
| cleanUpInternalState(); |
| principal = null; |
| } |
| else |
| { |
| // overall authentication succeeded and commit succeeded, |
| // but someone else's commit failed |
| logout(); |
| } |
| return true; |
| } |
| |
| /** |
| * Logout the user. |
| * |
| * This method removes the principal that was added by the |
| * {@code}commit{@code} method. |
| * |
| * @return true in all cases since this {@code}LoginModule{@code} |
| * should not be ignored. |
| * @throws LoginException if the logout fails. |
| */ |
| @Override |
| public boolean logout() throws LoginException |
| { |
| subject.getPrincipals().remove(principal); |
| succeeded = false; |
| cleanUpInternalState(); |
| principal = null; |
| return true; |
| } |
| |
| private void cleanUpInternalState() |
| { |
| username = null; |
| if (password != null) |
| { |
| for (int i = 0; i < password.length; i++) |
| password[i] = ' '; |
| password = null; |
| } |
| } |
| } |