com.sun.enterprise.admin.util.AdminLoginModule Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2012-2017 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.com/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.enterprise.admin.util;
import com.sun.enterprise.config.serverbeans.Domain;
import com.sun.enterprise.config.serverbeans.SecureAdmin;
import com.sun.enterprise.config.serverbeans.SecureAdminPrincipal;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import org.glassfish.common.util.admin.AdminAuthenticator;
import org.glassfish.common.util.admin.AuthTokenManager;
import org.glassfish.common.util.admin.RestSessionManager;
import org.glassfish.hk2.api.PerLookup;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.internal.api.LocalPassword;
import org.jvnet.hk2.annotations.Service;
/**
* Handles the non-username/password ways an admin user can authenticate.
*
* As specified by the LoginModule contract, the login method creates lists
* of principals or credentials to be added to the Subject during commit. Only
* if commit is invoked does the module actually add them to the Subject.
*
* @author tjquinn
*/
@Service
@PerLookup
public class AdminLoginModule implements LoginModule {
private static final Logger logger = GenericAdminAuthenticator.ADMSEC_LOGGER;
private static final Level PROGRESS_LEVEL = Level.FINE;
@Inject
private Domain domain;
@Inject
private AuthTokenManager authTokenManager;
@Inject
private LocalPassword localPassword;
@Inject
private RestSessionManager restSessionManager;
private SecureAdmin secureAdmin = null;
private boolean isAuthenticated;
private Subject subject;
private CallbackHandler callbackHandler;
// Holds principals and credentials that should be added to the real
// subject during commit, if it is ever invoked.
private final Subject subjectToAssemble = new Subject();
private final UsernamePasswordAuthenticator usernamePasswordAuth = new UsernamePasswordAuthenticator();
private final PrincipalAuthenticator principalAuth = new PrincipalAuthenticator();
private final AdminIndicatorAuthenticator adminIndicatorAuth = new AdminIndicatorAuthenticator();
private final AdminTokenAuthenticator adminTokenAuth = new AdminTokenAuthenticator();
private final RemoteHostAuthenticator remoteHostAuth = new RemoteHostAuthenticator();
private final RestAdminAuthenticator restTokenAuthenticator = new RestAdminAuthenticator();
private List callbacks = new ArrayList(Arrays.asList(
usernamePasswordAuth.nameCB,
usernamePasswordAuth.pwCB,
principalAuth.cb,
adminIndicatorAuth.cb,
adminTokenAuth.cb,
remoteHostAuth.cb,
restTokenAuthenticator.restTokenCB,
restTokenAuthenticator.remoteAddrCB));
private List authenticators = new ArrayList(Arrays.asList(
usernamePasswordAuth,
principalAuth,
adminIndicatorAuth,
adminTokenAuth,
restTokenAuthenticator));
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) {
if (callbackHandler instanceof AdminCallbackHandler) {
ServiceLocator sl = ((AdminCallbackHandler) callbackHandler).getServiceLocator();
findServices(sl);
}
this.subject = subject;
this.callbackHandler = callbackHandler;
}
private void findServices(final ServiceLocator sl) {
domain = sl.getService(Domain.class);
secureAdmin = domain.getSecureAdmin();
authTokenManager = sl.getService(AuthTokenManager.class);
localPassword = sl.getService(LocalPassword.class);
restSessionManager = sl.getService(RestSessionManager.class);
}
@Override
public boolean login() throws LoginException {
/*
* Without a callback handler we cannot find out what we need about
* the incoming request.
*/
if (callbackHandler == null) {
throw new LoginException(Strings.get("secure.admin.noCallbackHandler"));
}
try {
callbackHandler.handle(callbacks.toArray(new Callback[callbacks.size()]));
} catch (Exception ex) {
final LoginException lex = new LoginException();
lex.initCause(ex);
throw lex;
}
/*
* Make sure this login module has some way of authenticating this user.
* Otherwise we don't need it to be invoked during commit or logout.
*/
isAuthenticated = false;
for (AdminAuthenticator auth : authenticators) {
isAuthenticated |= auth.identify(subjectToAssemble);
}
logger.log(PROGRESS_LEVEL, "login returning {0}", isAuthenticated);
if ( ! isAuthenticated) {
throw new LoginException();
}
return isAuthenticated;
}
private void updateFromSubject(final Subject subjectToAddTo, final Subject subjectToAddFrom) {
subjectToAddTo.getPrincipals().addAll(subjectToAddFrom.getPrincipals());
subjectToAddTo.getPrivateCredentials().addAll(subjectToAddFrom.getPrivateCredentials());
subjectToAddTo.getPublicCredentials().addAll(subjectToAddFrom.getPublicCredentials());
}
@Override
public boolean commit() throws LoginException {
if ( ! isAuthenticated) {
return false;
}
updateFromSubject(subject, subjectToAssemble);
logger.log(PROGRESS_LEVEL, "commiting");
final Level dumpLevel = Level.FINER;
if (logger.isLoggable(dumpLevel)) {
logger.log(dumpLevel, "Following identity attached to subject: {0} principals, {1} private credentials, {2} public credentials",
new Object[] {subjectToAssemble.getPrincipals().size(),
subjectToAssemble.getPrivateCredentials().size(),
subjectToAssemble.getPublicCredentials().size()});
for (Principal p : subjectToAssemble.getPrincipals()) {
logger.log(dumpLevel, " principal: {0}", p.getName());
}
for (Object c : subjectToAssemble.getPrivateCredentials()) {
logger.log(dumpLevel, " private credential: {0}", c.toString());
}
for (Object c : subjectToAssemble.getPublicCredentials()) {
logger.log(dumpLevel, " public credential: {0}", c.toString());
}
}
return true;
}
@Override
public boolean abort() throws LoginException {
if ( ! isAuthenticated) {
return false;
}
logger.log(PROGRESS_LEVEL, "aborting");
removeAddedInfo();
return true;
}
@Override
public boolean logout() throws LoginException {
logger.log(PROGRESS_LEVEL, "logging out");
removeAddedInfo();
return true;
}
private void removeAddedInfo() {
subject.getPrincipals().removeAll(subjectToAssemble.getPrincipals());
subject.getPrivateCredentials().removeAll(subjectToAssemble.getPrivateCredentials());
subject.getPublicCredentials().removeAll(subjectToAssemble.getPublicCredentials());
}
static class PrincipalCallback implements Callback {
private Principal p;
public void setPrincipal(final Principal p) {
this.p = p;
}
public Principal getPrincipal() {
return p;
}
}
/*
* If the admin client sent the unique domain identifier in a header then
* that should mean the request came from another GlassFish server in this
* domain. Make sure that the value, if present, matches the one in
* this server's domain config. If they do not match then reject the
* message - it came from a domain other than this server's.
*
* Note that we don't insist that every request have the domain identifier. For
* example, requests from asadmin will not include the domain ID. But if
* the domain ID is present in the request it needs to match the
* configured ID.
*/
private static class SpecialAdminIndicatorChecker {
private static enum Result {
NOT_IN_REQUEST,
MATCHED,
MISMATCHED
}
private final SpecialAdminIndicatorChecker.Result _result;
private SpecialAdminIndicatorChecker(
final String actualIndicator,
final String expectedIndicator,
final String originHost) {
final Level dumpLevel = Level.FINER;
if (actualIndicator != null) {
if (actualIndicator.equals(expectedIndicator)) {
_result = SpecialAdminIndicatorChecker.Result.MATCHED;
logger.log(dumpLevel, "Admin request contains expected domain ID");
} else {
logger.log(Level.WARNING, AdminLoggerInfo.mForeignDomainID,
new Object[] { originHost, actualIndicator, expectedIndicator});
_result = SpecialAdminIndicatorChecker.Result.MISMATCHED;
}
} else {
logger.log(dumpLevel, "Admin request contains no domain ID; this is OK - continuing");
_result = SpecialAdminIndicatorChecker.Result.NOT_IN_REQUEST;
}
}
private SpecialAdminIndicatorChecker.Result result() {
return _result;
}
}
abstract class Authenticator implements AdminAuthenticator {
private final AuthenticatorType type;
final Callback cb;
Authenticator(final AuthenticatorType type, final Callback cb) {
this.type = type;
this.cb = cb;
}
@Override
public List callbacks() {
return new ArrayList(Arrays.asList(cb));
}
@Override
public AuthenticatorType type() {
return type;
}
}
abstract class TextAuthenticator extends Authenticator {
final TextInputCallback textCB;
TextAuthenticator(final AuthenticatorType type) {
super(type, new TextInputCallback(type.name()));
textCB = (TextInputCallback) cb;
}
}
class PrincipalAuthenticator extends Authenticator {
final private PrincipalCallback pcb;
PrincipalAuthenticator() {
super(AuthenticatorType.PRINCIPAL, new PrincipalCallback());
pcb = (PrincipalCallback) cb;
}
@Override
public boolean identify(Subject subject) {
/*
* There are three ways certs can be attached to the request:
* 1. A non-GlassFish client (a human) actually sent a cert.
*
* 2. One server is sending another server a request. In this case
* the cert's DN should be among the secure-admin-principals.
*
* 3. The request came from the console and secure admin is enabled,
* in which case (because the console runs in the DAS
* and sends ReST requests over the net to the DAS) the request
* will have both the DAS's cert and the user's username and password.
*/
final Principal p = pcb.getPrincipal();
if (p != null) {
if (isPrincipalFromGlassFish(p) && usernamePasswordAuth.isActive()) {
/*
* This is the console case. Do not indicate that the client
* is identifiable using an SSL cert. Instead rely on
* the username/password authentication code path.
*/
logger.log(PROGRESS_LEVEL, "Detected console request - not adding SSL principal to the subject");
return false;
}
/*
* In all other cases add the principal to the tentative subject.
*/
subject.getPrincipals().add(p);
logger.log(PROGRESS_LEVEL, "Attaching Principal {0}", p.getName());
}
return p != null;
}
private boolean isPrincipalFromGlassFish(final Principal p) {
for (SecureAdminPrincipal sap : secureAdmin.getSecureAdminPrincipal()) {
if (sap.getDn().equals(p.getName())) {
return true;
}
}
return false;
}
}
class AdminIndicatorAuthenticator extends TextAuthenticator {
AdminIndicatorAuthenticator() {
super(AuthenticatorType.ADMIN_INDICATOR);
}
@Override
public boolean identify(Subject subject) throws LoginException {
if (secureAdmin == null) {
return false;
}
final String providedIndicator = textCB.getText();
final SpecialAdminIndicatorChecker checker = new SpecialAdminIndicatorChecker(
providedIndicator,
secureAdmin.getSpecialAdminIndicator(),
remoteHostAuth.textCB.getText());
if (checker.result() == SpecialAdminIndicatorChecker.Result.MISMATCHED) {
throw new LoginException();
}
/*
* Either there was no special indicator or there was one and
* it matched what we expect.
*/
if (checker.result() == SpecialAdminIndicatorChecker.Result.MATCHED) {
/*
* Add a principal indicating that this subject represents
* another server in the domain and so we
* can trust it completely, even if the request is remote and secure
* admin is disabled.
*/
subject.getPrincipals().add(new AdminIndicatorPrincipal(providedIndicator));
return true;
}
return false;
}
}
class AdminTokenAuthenticator extends TextAuthenticator {
AdminTokenAuthenticator() {
super(AuthenticatorType.ADMIN_TOKEN);
}
@Override
public boolean identify(Subject subject) throws LoginException {
if (authTokenManager == null) {
return false;
}
Subject s = null;
final String token = textCB.getText();
if (token != null) {
s = authTokenManager.findToken(token);
if (s != null) {
/*
* The token manager knows which Subject was effective when the token
* was created. We add those to the lists we'll add if this module's
* commit is invoked.
*/
logger.log(PROGRESS_LEVEL, "Recognized valid limited-use token");
updateFromSubject(subject, s);
/*
* Add an additional principal indicating that we trust this
* subject to make remote requests. Otherwise we would
* reject attempts to use a token from off-node, and that's
* partly the whole point of tokens.
*/
subject.getPrincipals().add(new AdminTokenPrincipal(token));
}
}
return s != null;
}
}
class UsernamePasswordAuthenticator extends Authenticator {
final NameCallback nameCB = new NameCallback("username");
final PasswordCallback pwCB = new PasswordCallback("password", false);
UsernamePasswordAuthenticator() {
super(AuthenticatorType.USERNAME_PASSWORD, null);
}
boolean isActive() {
return nameCB.getName() != null || pwCB.getPassword() != null;
}
@Override
public boolean identify(final Subject subject) throws LoginException {
/*
* Note that this LoginModule does not authenticate the normal
* username/password pairs. That's left to another one. This module
* checks for the local password.
*/
if (localPassword == null) {
return false;
}
final boolean result = localPassword.isLocalPassword(new String(pwCB.getPassword()));
if (result) {
subject.getPrincipals().add(new AdminLocalPasswordPrincipal());
logger.log(PROGRESS_LEVEL, "AdminLoginModule detected local password");
}
return result;
}
@Override
public List callbacks() {
return new ArrayList(Arrays.asList(nameCB, pwCB));
}
}
class RemoteHostAuthenticator extends TextAuthenticator {
RemoteHostAuthenticator() {
super(AuthenticatorType.REMOTE_HOST);
}
@Override
public boolean identify(final Subject subject) throws LoginException {
return false;
}
}
class RestAdminAuthenticator extends Authenticator {
private TextInputCallback restTokenCB = new TextInputCallback(AdminAuthenticator.REST_TOKEN_NAME);
private TextInputCallback remoteAddrCB = new TextInputCallback(AdminAuthenticator.REMOTE_ADDR_NAME);
RestAdminAuthenticator() {
super(AuthenticatorType.REST_TOKEN, null);
}
@Override
public List callbacks() {
return new ArrayList(Arrays.asList(cb, remoteAddrCB));
}
@Override
public boolean identify(final Subject subject) throws LoginException {
if (restSessionManager == null) {
return false;
}
boolean result = false;
final String token = restTokenCB.getText();
final String remoteAddr = remoteAddrCB.getText();
if (token != null && remoteAddr != null) {
final Subject s = restSessionManager.authenticate(token, remoteAddr);
if (s != null) {
result = true;
updateFromSubject(subject, s);
logger.log(PROGRESS_LEVEL, "Detected ReST token");
}
}
return result;
}
}
}