PageRenderTime 42ms CodeModel.GetById 27ms app.highlight 12ms RepoModel.GetById 1ms app.codeStats 0ms

/connector/src/main/java/org/jboss/as/connector/security/ElytronCallbackHandler.java

https://github.com/ochaloup/jboss-as
Java | 285 lines | 192 code | 27 blank | 66 comment | 63 complexity | c1a0035941730bff1ba721c952a3c84f MD5 | raw file
  1/*
  2 * Copyright 2017 Red Hat, Inc.
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License");
  5 * you may not use this file except in compliance with the License.
  6 * You may obtain a copy of the License at
  7 *
  8 *   http://www.apache.org/licenses/LICENSE-2.0
  9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS,
 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 * See the License for the specific language governing permissions and
 14 * limitations under the License.
 15 */
 16package org.jboss.as.connector.security;
 17
 18import static org.jboss.as.connector.logging.ConnectorLogger.SUBSYSTEM_RA_LOGGER;
 19
 20import java.io.IOException;
 21import java.io.Serializable;
 22import java.security.AccessController;
 23import java.security.Principal;
 24import java.security.PrivilegedAction;
 25import java.util.Arrays;
 26import java.util.HashSet;
 27import java.util.Set;
 28
 29import javax.resource.spi.security.PasswordCredential;
 30import javax.security.auth.Subject;
 31import javax.security.auth.callback.CallbackHandler;
 32import javax.security.auth.callback.UnsupportedCallbackException;
 33import javax.security.auth.message.callback.CallerPrincipalCallback;
 34import javax.security.auth.message.callback.GroupPrincipalCallback;
 35import javax.security.auth.message.callback.PasswordValidationCallback;
 36
 37import org.jboss.jca.core.spi.security.Callback;
 38import org.wildfly.security.auth.principal.NamePrincipal;
 39import org.wildfly.security.auth.server.RealmUnavailableException;
 40import org.wildfly.security.auth.server.SecurityDomain;
 41import org.wildfly.security.auth.server.SecurityIdentity;
 42import org.wildfly.security.auth.server.ServerAuthenticationContext;
 43import org.wildfly.security.authz.RoleMapper;
 44import org.wildfly.security.authz.Roles;
 45import org.wildfly.security.evidence.PasswordGuessEvidence;
 46import org.wildfly.security.manager.WildFlySecurityManager;
 47
 48/**
 49 * An Elytron based {@link CallbackHandler} implementation designed for the Jakarta Connectors security inflow. It uses the information
 50 * obtained from the {@link javax.security.auth.callback.Callback}s to authenticate and authorize the identity supplied
 51 * by the resource adapter and inserts the {@link SecurityIdentity} representing the authorized identity in the subject's
 52 * private credentials set.
 53 *
 54 * @author Flavia Rainone
 55 * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
 56 */
 57public class ElytronCallbackHandler implements CallbackHandler, Serializable {
 58
 59    private final SecurityDomain securityDomain;
 60
 61    private final Callback mappings;
 62
 63    private Subject executionSubject;
 64
 65    /**
 66     * Constructor
 67     * @param securityDomain the Elytron security domain used to establish the caller principal.
 68     * @param mappings The mappings.
 69     */
 70    public ElytronCallbackHandler(final SecurityDomain securityDomain, final Callback mappings) {
 71        this.securityDomain = securityDomain;
 72        this.mappings = mappings;
 73    }
 74
 75    /**
 76     * {@inheritDoc}
 77     */
 78    public void handle(javax.security.auth.callback.Callback[] callbacks) throws UnsupportedCallbackException, IOException {
 79        if (SUBSYSTEM_RA_LOGGER.isTraceEnabled())
 80            SUBSYSTEM_RA_LOGGER.elytronHandlerHandle(Arrays.toString(callbacks));
 81
 82        // work wrapper calls the callback handler a second time with default callback values after the handler was invoked
 83        // by the RA. We must check if the execution subject already contains an identity and allow for replacement of the
 84        // identity with values found in the default callbacks only if the subject has no identity yet or if the identity
 85        // is the anonymous one.
 86        if (this.executionSubject != null) {
 87            final SecurityIdentity subjectIdentity = this.getPrivateCredential(this.executionSubject, SecurityIdentity.class);
 88            if (subjectIdentity != null && !subjectIdentity.isAnonymous()) {
 89                return;
 90            }
 91        }
 92
 93        if (callbacks != null && callbacks.length > 0)
 94        {
 95            if (this.mappings != null && this.mappings.isMappingRequired())
 96            {
 97                callbacks = this.mappings.mapCallbacks(callbacks);
 98            }
 99
100            GroupPrincipalCallback groupPrincipalCallback = null;
101            CallerPrincipalCallback callerPrincipalCallback = null;
102            PasswordValidationCallback passwordValidationCallback = null;
103
104            for (javax.security.auth.callback.Callback callback : callbacks) {
105                if (callback instanceof GroupPrincipalCallback) {
106                    groupPrincipalCallback = (GroupPrincipalCallback) callback;
107                    if (this.executionSubject == null) {
108                        this.executionSubject = groupPrincipalCallback.getSubject();
109                    } else if (!this.executionSubject.equals(groupPrincipalCallback.getSubject())) {
110                        // TODO merge the contents of the subjects?
111                    }
112                } else if (callback instanceof CallerPrincipalCallback) {
113                    callerPrincipalCallback = (CallerPrincipalCallback) callback;
114                    if (this.executionSubject == null) {
115                        this.executionSubject = callerPrincipalCallback.getSubject();
116                    } else if (!this.executionSubject.equals(callerPrincipalCallback.getSubject())) {
117                        // TODO merge the contents of the subjects?
118                    }
119                } else if (callback instanceof PasswordValidationCallback) {
120                    passwordValidationCallback = (PasswordValidationCallback) callback;
121                    if (this.executionSubject == null) {
122                        this.executionSubject = passwordValidationCallback.getSubject();
123                    } else if (!this.executionSubject.equals(passwordValidationCallback.getSubject())) {
124                        // TODO merge the contents of the subjects?
125                    }
126                } else {
127                    throw new UnsupportedCallbackException(callback);
128                }
129            }
130            this.handleInternal(callerPrincipalCallback, groupPrincipalCallback, passwordValidationCallback);
131        }
132    }
133
134    protected void handleInternal(final CallerPrincipalCallback callerPrincipalCallback, final GroupPrincipalCallback groupPrincipalCallback,
135                                  final PasswordValidationCallback passwordValidationCallback) throws IOException {
136
137        if(this.executionSubject == null) {
138            throw SUBSYSTEM_RA_LOGGER.executionSubjectNotSetInHandler();
139        }
140        SecurityIdentity identity = this.securityDomain.getAnonymousSecurityIdentity();
141
142        // establish the caller principal using the info from the callback.
143        Principal callerPrincipal = null;
144        if (callerPrincipalCallback != null) {
145            Principal callbackPrincipal = callerPrincipalCallback.getPrincipal();
146            callerPrincipal = callbackPrincipal != null ? new NamePrincipal(callbackPrincipal.getName()) :
147                    callerPrincipalCallback.getName() != null ? new NamePrincipal(callerPrincipalCallback.getName()) : null;
148        }
149
150        // a null principal is the ra contract for requiring the use of the unauthenticated identity - no point in attempting to authenticate.
151        if (callerPrincipal != null) {
152            // check if we have a username/password pair to authenticate - first try the password validation callback.
153            if (passwordValidationCallback != null) {
154                final String username = passwordValidationCallback.getUsername();
155                final char[] password = passwordValidationCallback.getPassword();
156                try {
157                    identity = this.authenticate(username, password);
158                    // add a password credential to the execution subject and set the successful result in the callback.
159                    this.addPrivateCredential(this.executionSubject, new PasswordCredential(username, password));
160                    passwordValidationCallback.setResult(true);
161                } catch (SecurityException e) {
162                    passwordValidationCallback.setResult(false);
163                    return;
164                }
165            } else {
166                // identity not established using the callback - check if the execution subject contains a password credential.
167                PasswordCredential passwordCredential = this.getPrivateCredential(this.executionSubject, PasswordCredential.class);
168                if (passwordCredential != null) {
169                    try {
170                        identity = this.authenticate(passwordCredential.getUserName(), passwordCredential.getPassword());
171                    } catch (SecurityException e) {
172                        return;
173                    }
174                } else {
175                    identity = securityDomain.createAdHocIdentity(callerPrincipal);
176                }
177            }
178
179            // at this point we either have an authenticated identity or an anonymous one. We must now check if the caller principal
180            // is different from the identity principal and switch to the caller principal identity if needed.
181            if (!callerPrincipal.equals(identity.getPrincipal())) {
182                identity = identity.createRunAsIdentity(callerPrincipal.getName());
183            }
184
185            // if we have new roles coming from the group callback, set a new mapper in the identity.
186            if (groupPrincipalCallback != null) {
187                String[] groups = groupPrincipalCallback.getGroups();
188                if (groups != null) {
189                    Set<String> roles = new HashSet<>(Arrays.asList(groups));
190                    // TODO what category should we use here?
191                    identity = identity.withRoleMapper(ElytronSecurityIntegration.SECURITY_IDENTITY_ROLE, RoleMapper.constant(Roles.fromSet(roles)));
192                }
193            }
194        }
195
196        // set the authenticated identity as a private credential in the subject.
197        this.executionSubject.getPrincipals().add(identity.getPrincipal());
198        this.addPrivateCredential(executionSubject, identity);
199    }
200
201    /**
202     * Authenticate the user with the given credential against the configured Elytron security domain.
203     *
204     * @param username the user being authenticated.
205     * @param credential the credential used as evidence to verify the user's identity.
206     * @return the authenticated and authorized {@link SecurityIdentity}.
207     * @throws IOException if an error occurs while authenticating the user.
208     */
209    private SecurityIdentity authenticate(final String username, final char[] credential) throws IOException {
210        final ServerAuthenticationContext context = this.securityDomain.createNewAuthenticationContext();
211        final PasswordGuessEvidence evidence = new PasswordGuessEvidence(credential != null ? credential : null);
212        try {
213            context.setAuthenticationName(username);
214            if (context.verifyEvidence(evidence)) {
215                if (context.authorize()) {
216                    context.succeed();
217                    return context.getAuthorizedIdentity();
218                } else {
219                    context.fail();
220                    throw new SecurityException("Authorization failed");
221                }
222            } else {
223                context.fail();
224                throw new SecurityException("Authentication failed");
225            }
226        } catch (IllegalArgumentException | IllegalStateException | RealmUnavailableException e) {
227            context.fail();
228            throw e;
229        } finally {
230            if (!context.isDone()) {
231                context.fail();
232            }
233            evidence.destroy();
234        }
235    }
236
237    protected<T> T getPrivateCredential(final Subject subject, final Class<T> credentialClass) {
238        T credential = null;
239        if (subject != null) {
240            Set<T> credentialSet;
241            if (!WildFlySecurityManager.isChecking()) {
242                credentialSet = subject.getPrivateCredentials(credentialClass);
243            } else {
244                credentialSet = AccessController.doPrivileged((PrivilegedAction<Set<T>>) () ->
245                        subject.getPrivateCredentials(credentialClass));
246            }
247            if (!credentialSet.isEmpty()) {
248                credential = credentialSet.iterator().next();
249            }
250        }
251        return credential;
252    }
253
254    /**
255     * Add the specified credential to the subject's private credentials set.
256     *
257     * @param subject the {@link Subject} to add the credential to.
258     * @param credential a reference to the credential.
259     */
260    protected void addPrivateCredential(final Subject subject, final Object credential) {
261        if (!WildFlySecurityManager.isChecking()) {
262            subject.getPrivateCredentials().add(credential);
263        }
264        else {
265            AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
266                subject.getPrivateCredentials().add(credential);
267                return null;
268            });
269        }
270    }
271
272    /**
273     * {@inheritDoc}
274     */
275    @Override
276    public String toString() {
277        StringBuilder sb = new StringBuilder();
278
279        sb.append("ElytronCallbackHandler@").append(Integer.toHexString(System.identityHashCode(this)));
280        sb.append("[mappings=").append(mappings);
281        sb.append("]");
282
283        return sb.toString();
284    }
285}