/*
* Copyright 2017 Red Hat, Inc.
*
* 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 org.jboss.as.connector.security;
import static org.jboss.as.connector.logging.ConnectorLogger.SUBSYSTEM_RA_LOGGER;
import java.io.IOException;
import java.io.Serializable;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.resource.spi.security.PasswordCredential;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.message.callback.CallerPrincipalCallback;
import javax.security.auth.message.callback.GroupPrincipalCallback;
import javax.security.auth.message.callback.PasswordValidationCallback;
import org.jboss.jca.core.spi.security.Callback;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.server.SecurityDomain;
import org.wildfly.security.auth.server.SecurityIdentity;
import org.wildfly.security.auth.server.ServerAuthenticationContext;
import org.wildfly.security.authz.RoleMapper;
import org.wildfly.security.authz.Roles;
import org.wildfly.security.evidence.PasswordGuessEvidence;
import org.wildfly.security.manager.WildFlySecurityManager;
/**
* An Elytron based {@link CallbackHandler} implementation designed for the JCA security inflow. It uses the information
* obtained from the {@link javax.security.auth.callback.Callback}s to authenticate and authorize the identity supplied
* by the resource adapter and inserts the {@link SecurityIdentity} representing the authorized identity in the subject's
* private credentials set.
*
* @author Flavia Rainone
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class ElytronCallbackHandler implements CallbackHandler, Serializable {
private final SecurityDomain securityDomain;
private final Callback mappings;
private Subject executionSubject;
/**
* Constructor
* @param securityDomain the Elytron security domain used to establish the caller principal.
* @param mappings The mappings.
*/
public ElytronCallbackHandler(final SecurityDomain securityDomain, final Callback mappings) {
this.securityDomain = securityDomain;
this.mappings = mappings;
}
/**
* {@inheritDoc}
*/
public void handle(javax.security.auth.callback.Callback[] callbacks) throws UnsupportedCallbackException, IOException {
if (SUBSYSTEM_RA_LOGGER.isTraceEnabled())
SUBSYSTEM_RA_LOGGER.elytronHandlerHandle(Arrays.toString(callbacks));
// work wrapper calls the callback handler a second time with default callback values after the handler was invoked
// by the RA. We must check if the execution subject already contains an identity and allow for replacement of the
// identity with values found in the default callbacks only if the subject has no identity yet or if the identity
// is the anonymous one.
if (this.executionSubject != null) {
final SecurityIdentity subjectIdentity = this.getPrivateCredential(this.executionSubject, SecurityIdentity.class);
if (subjectIdentity != null && !subjectIdentity.isAnonymous()) {
return;
}
}
if (callbacks != null && callbacks.length > 0)
{
if (this.mappings != null && this.mappings.isMappingRequired())
{
callbacks = this.mappings.mapCallbacks(callbacks);
}
GroupPrincipalCallback groupPrincipalCallback = null;
CallerPrincipalCallback callerPrincipalCallback = null;
PasswordValidationCallback passwordValidationCallback = null;
for (javax.security.auth.callback.Callback callback : callbacks) {
if (callback instanceof GroupPrincipalCallback) {
groupPrincipalCallback = (GroupPrincipalCallback) callback;
if (this.executionSubject == null) {
this.executionSubject = groupPrincipalCallback.getSubject();
} else if (!this.executionSubject.equals(groupPrincipalCallback.getSubject())) {
// TODO merge the contents of the subjects?
}
} else if (callback instanceof CallerPrincipalCallback) {
callerPrincipalCallback = (CallerPrincipalCallback) callback;
if (this.executionSubject == null) {
this.executionSubject = callerPrincipalCallback.getSubject();
} else if (!this.executionSubject.equals(callerPrincipalCallback.getSubject())) {
// TODO merge the contents of the subjects?
}
} else if (callback instanceof PasswordValidationCallback) {
passwordValidationCallback = (PasswordValidationCallback) callback;
if (this.executionSubject == null) {
this.executionSubject = passwordValidationCallback.getSubject();
} else if (!this.executionSubject.equals(passwordValidationCallback.getSubject())) {
// TODO merge the contents of the subjects?
}
} else {
throw new UnsupportedCallbackException(callback);
}
}
this.handleInternal(callerPrincipalCallback, groupPrincipalCallback, passwordValidationCallback);
}
}
protected void handleInternal(final CallerPrincipalCallback callerPrincipalCallback, final GroupPrincipalCallback groupPrincipalCallback,
final PasswordValidationCallback passwordValidationCallback) throws IOException {
if(this.executionSubject == null) {
throw SUBSYSTEM_RA_LOGGER.executionSubjectNotSetInHandler();
}
SecurityIdentity identity = this.securityDomain.getAnonymousSecurityIdentity();
// establish the caller principal using the info from the callback.
Principal callerPrincipal = null;
if (callerPrincipalCallback != null) {
Principal callbackPrincipal = callerPrincipalCallback.getPrincipal();
callerPrincipal = callbackPrincipal != null ? new NamePrincipal(callbackPrincipal.getName()) :
callerPrincipalCallback.getName() != null ? new NamePrincipal(callerPrincipalCallback.getName()) : null;
}
// a null principal is the ra contract for requiring the use of the unauthenticated identity - no point in attempting to authenticate.
if (callerPrincipal != null) {
// check if we have a username/password pair to authenticate - first try the password validation callback.
if (passwordValidationCallback != null) {
final String username = passwordValidationCallback.getUsername();
final char[] password = passwordValidationCallback.getPassword();
try {
identity = this.authenticate(username, password);
// add a password credential to the execution subject and set the successful result in the callback.
this.addPrivateCredential(this.executionSubject, new PasswordCredential(username, password));
passwordValidationCallback.setResult(true);
} catch (SecurityException e) {
passwordValidationCallback.setResult(false);
return;
}
} else {
// identity not established using the callback - check if the execution subject contains a password credential.
PasswordCredential passwordCredential = this.getPrivateCredential(this.executionSubject, PasswordCredential.class);
if (passwordCredential != null) {
try {
identity = this.authenticate(passwordCredential.getUserName(), passwordCredential.getPassword());
} catch (SecurityException e) {
return;
}
} else {
identity = securityDomain.createAdHocIdentity(callerPrincipal);
}
}
// at this point we either have an authenticated identity or an anonymous one. We must now check if the caller principal
// is different from the identity principal and switch to the caller principal identity if needed.
if (!callerPrincipal.equals(identity.getPrincipal())) {
identity = identity.createRunAsIdentity(callerPrincipal.getName());
}
// if we have new roles coming from the group callback, set a new mapper in the identity.
if (groupPrincipalCallback != null) {
String[] groups = groupPrincipalCallback.getGroups();
if (groups != null) {
Set<String> roles = new HashSet<>(Arrays.asList(groups));
// TODO what category should we use here?
identity = identity.withRoleMapper(ElytronSecurityIntegration.SECURITY_IDENTITY_ROLE, RoleMapper.constant(Roles.fromSet(roles)));
}
}
}
// set the authenticated identity as a private credential in the subject.
this.executionSubject.getPrincipals().add(identity.getPrincipal());
this.addPrivateCredential(executionSubject, identity);
}
/**
* Authenticate the user with the given credential against the configured Elytron security domain.
*
* @param username the user being authenticated.
* @param credential the credential used as evidence to verify the user's identity.
* @return the authenticated and authorized {@link SecurityIdentity}.
* @throws IOException if an error occurs while authenticating the user.
*/
private SecurityIdentity authenticate(final String username, final char[] credential) throws IOException {
final ServerAuthenticationContext context = this.securityDomain.createNewAuthenticationContext();
final PasswordGuessEvidence evidence = new PasswordGuessEvidence(credential != null ? credential : null);
try {
context.setAuthenticationName(username);
if (context.verifyEvidence(evidence)) {
if (context.authorize()) {
context.succeed();
return context.getAuthorizedIdentity();
} else {
context.fail();
throw new SecurityException("Authorization failed");
}
} else {
context.fail();
throw new SecurityException("Authentication failed");
}
} catch (IllegalArgumentException | IllegalStateException | RealmUnavailableException e) {
context.fail();
throw e;
} finally {
if (!context.isDone()) {
context.fail();
}
evidence.destroy();
}
}
protected<T> T getPrivateCredential(final Subject subject, final Class<T> credentialClass) {
T credential = null;
if (subject != null) {
Set<T> credentialSet;
if (!WildFlySecurityManager.isChecking()) {
credentialSet = subject.getPrivateCredentials(credentialClass);
} else {
credentialSet = AccessController.doPrivileged((PrivilegedAction<Set<T>>) () ->
subject.getPrivateCredentials(credentialClass));
}
if (!credentialSet.isEmpty()) {
credential = credentialSet.iterator().next();
}
}
return credential;
}
/**
* Add the specified credential to the subject's private credentials set.
*
* @param subject the {@link Subject} to add the credential to.
* @param credential a reference to the credential.
*/
protected void addPrivateCredential(final Subject subject, final Object credential) {
if (!WildFlySecurityManager.isChecking()) {
subject.getPrivateCredentials().add(credential);
}
else {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
subject.getPrivateCredentials().add(credential);
return null;
});
}
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ElytronCallbackHandler@").append(Integer.toHexString(System.identityHashCode(this)));
sb.append("[mappings=").append(mappings);
sb.append("]");
return sb.toString();
}
}