/* * JBoss, Home of Professional Open Source * * Copyright 2015 Red Hat, Inc. and/or its affiliates. * * 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.wildfly.security.auth.realm.jdbc; import org.wildfly.common.Assert; import org.wildfly.security.auth.principal.NamePrincipal; import org.wildfly.security.auth.realm.CacheableSecurityRealm; import org.wildfly.security.auth.realm.jdbc.mapper.AttributeMapper; import org.wildfly.security.auth.server.RealmIdentity; import org.wildfly.security.auth.server.RealmUnavailableException; import org.wildfly.security.auth.SupportLevel; import org.wildfly.security.authz.Attributes; import org.wildfly.security.authz.AuthorizationIdentity; import org.wildfly.security.authz.MapAttributes; import org.wildfly.security.credential.Credential; import org.wildfly.security.evidence.Evidence; import javax.sql.DataSource; import java.security.Principal; import java.security.Provider; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; import static org.wildfly.security._private.ElytronMessages.log; /** * Security realm implementation backed by a database. * * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> */ public class JdbcSecurityRealm implements CacheableSecurityRealm { private final Supplier<Provider[]> providers; private final List<QueryConfiguration> queryConfiguration; public static JdbcSecurityRealmBuilder builder() { return new JdbcSecurityRealmBuilder(); } JdbcSecurityRealm(List<QueryConfiguration> queryConfiguration, Supplier<Provider[]> providers) { this.queryConfiguration = queryConfiguration; this.providers = providers; } @Override public RealmIdentity getRealmIdentity(final Principal principal) { if (! (principal instanceof NamePrincipal)) { return RealmIdentity.NON_EXISTENT; } return new JdbcRealmIdentity(principal.getName()); } @Override public SupportLevel getCredentialAcquireSupport(final Class<? extends Credential> credentialType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("credentialType", credentialType); SupportLevel support = SupportLevel.UNSUPPORTED; for (QueryConfiguration configuration : queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { final SupportLevel mapperSupport = keyMapper.getCredentialAcquireSupport(credentialType, algorithmName); if (support.compareTo(mapperSupport) < 0) { support = mapperSupport; } } } return support; } @Override public SupportLevel getEvidenceVerifySupport(final Class<? extends Evidence> evidenceType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("evidenceType", evidenceType); SupportLevel support = SupportLevel.UNSUPPORTED; for (QueryConfiguration configuration : queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { final SupportLevel mapperSupport = keyMapper.getEvidenceVerifySupport(evidenceType, algorithmName); if (support.compareTo(mapperSupport) < 0) { support = mapperSupport; } } } return support; } @Override public void registerIdentityChangeListener(Consumer<Principal> listener) { // no notifications from this realm about changes on the underlying storage } private class JdbcRealmIdentity implements RealmIdentity { private final String name; private JdbcIdentity identity; public JdbcRealmIdentity(String name) { this.name = name; } public Principal getRealmIdentityPrincipal() { return new NamePrincipal(name); } @Override public SupportLevel getCredentialAcquireSupport(final Class<? extends Credential> credentialType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("credentialType", credentialType); SupportLevel support = SupportLevel.UNSUPPORTED; for (QueryConfiguration configuration : JdbcSecurityRealm.this.queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { if (keyMapper.getCredentialAcquireSupport(credentialType, algorithmName).mayBeSupported()) { final SupportLevel mapperSupport = executePrincipalQuery(configuration, r -> keyMapper.getCredentialSupport(r, providers)); if (mapperSupport == SupportLevel.SUPPORTED) { return SupportLevel.SUPPORTED; } else if (mapperSupport == SupportLevel.POSSIBLY_SUPPORTED) { support = SupportLevel.POSSIBLY_SUPPORTED; } } } } return support; } @Override public <C extends Credential> C getCredential(final Class<C> credentialType) throws RealmUnavailableException { return getCredential(credentialType, null); } @Override public <C extends Credential> C getCredential(final Class<C> credentialType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("credentialType", credentialType); for (QueryConfiguration configuration : JdbcSecurityRealm.this.queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { if (keyMapper.getCredentialAcquireSupport(credentialType, algorithmName).mayBeSupported()) { final Credential credential = executePrincipalQuery(configuration, r -> keyMapper.map(r, providers)); if (credentialType.isInstance(credential)) { return credentialType.cast(credential); } } } } return null; } @Override public SupportLevel getEvidenceVerifySupport(final Class<? extends Evidence> evidenceType, final String algorithmName) throws RealmUnavailableException { Assert.checkNotNullParam("evidenceType", evidenceType); SupportLevel support = SupportLevel.UNSUPPORTED; for (QueryConfiguration configuration : JdbcSecurityRealm.this.queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { if (keyMapper.getEvidenceVerifySupport(evidenceType, algorithmName).mayBeSupported()) { final SupportLevel mapperSupport = executePrincipalQuery(configuration, r -> keyMapper.getCredentialSupport(r, providers)); if (mapperSupport == SupportLevel.SUPPORTED) { return SupportLevel.SUPPORTED; } else if (mapperSupport == SupportLevel.POSSIBLY_SUPPORTED) { support = SupportLevel.POSSIBLY_SUPPORTED; } } } } return support; } @Override public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableException { Assert.checkNotNullParam("evidence", evidence); if (exists()) { for (Credential credential : this.identity.credentials) { if (credential.canVerify(evidence)) { return credential.verify(evidence); } } } return false; } public boolean exists() throws RealmUnavailableException { return getIdentity() != null; } @Override public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException { if (!exists()) { return AuthorizationIdentity.EMPTY; } return AuthorizationIdentity.basicIdentity(this.identity.attributes); } private JdbcIdentity getIdentity() { if (this.identity == null) { this.identity = JdbcSecurityRealm.this.queryConfiguration.stream().map(queryConfiguration -> executePrincipalQuery(queryConfiguration, resultSet -> { if (resultSet.next()) { MapAttributes attributes = new MapAttributes(); do { queryConfiguration.getColumnMappers(AttributeMapper.class).forEach(attributeMapper -> { try { Object value = attributeMapper.map(resultSet, providers); if (value != null) { attributes.addFirst(attributeMapper.getName(), value.toString()); } } catch (SQLException cause) { throw log.ldapRealmFailedObtainAttributes(this.name, cause); } }); } while (resultSet.next()); return attributes; } return null; })).collect(Collectors.reducing((lAttribute, rAttribute) -> { if (rAttribute == null) { return lAttribute; } MapAttributes attributes = new MapAttributes(lAttribute); for (Attributes.Entry rEntry : rAttribute.entries()) { attributes.get(rEntry.getKey()).addAll(rEntry); } return attributes; })).map(attributes -> { List<Credential> credentials = new ArrayList<>(); for (QueryConfiguration configuration : queryConfiguration) { for (KeyMapper keyMapper : configuration.getColumnMappers(KeyMapper.class)) { credentials.add(executePrincipalQuery(configuration, r -> keyMapper.map(r, providers))); } } return new JdbcIdentity(attributes, credentials); }).orElse(null); } return this.identity; } private Connection getConnection(QueryConfiguration configuration) { try { DataSource dataSource = configuration.getDataSource(); return dataSource.getConnection(); } catch (Exception e) { throw log.couldNotOpenConnection(e); } } private <E> E executePrincipalQuery(QueryConfiguration configuration, ResultSetCallback<E> resultSetCallback) { String sql = configuration.getSql(); log.tracef("Executing principalQuery %s with value %s", sql, name); try ( Connection connection = getConnection(configuration); PreparedStatement preparedStatement = connection.prepareStatement(sql) ) { preparedStatement.setString(1, name); try ( ResultSet resultSet = preparedStatement.executeQuery() ) { return resultSetCallback.handle(resultSet); } } catch (SQLException e) { throw log.couldNotExecuteQuery(sql, e); } catch (Exception e) { throw log.unexpectedErrorWhenProcessingAuthenticationQuery(sql, e); } } private class JdbcIdentity { private final Attributes attributes; private List<Credential> credentials = new ArrayList<>(); JdbcIdentity(Attributes attributes, List<Credential> credentials) { this.attributes = attributes; this.credentials = credentials; } } } private interface ResultSetCallback<E> { E handle(ResultSet resultSet) throws SQLException; } }