All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opendaylight.aaa.shiro.realm.KeystoneAuthRealm Maven / Gradle / Ivy

There is a newer version: 0.20.1
Show newest version
/*
 * Copyright (c) 2017 Ericsson Inc. and others.  All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
 * and is available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.opendaylight.aaa.shiro.realm;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.opendaylight.aaa.api.shiro.principal.ODLPrincipal;
import org.opendaylight.aaa.cert.api.ICertificateManager;
import org.opendaylight.aaa.provider.GsonProvider;
import org.opendaylight.aaa.shiro.keystone.domain.KeystoneAuth;
import org.opendaylight.aaa.shiro.keystone.domain.KeystoneToken;
import org.opendaylight.aaa.shiro.principal.ODLPrincipalImpl;
import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpClient;
import org.opendaylight.aaa.shiro.realm.util.http.SimpleHttpRequest;
import org.opendaylight.aaa.shiro.realm.util.http.UntrustedSSL;
import org.opendaylight.aaa.shiro.web.env.ThreadLocals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * KeystoneAuthRealm is a Shiro Realm that authenticates users from
 * OpenStack Keystone.
 */
public class KeystoneAuthRealm extends AuthorizingRealm {

    private static final Logger LOG = LoggerFactory.getLogger(KeystoneAuthRealm.class);

    private static final String NO_CATALOG_OPTION = "nocatalog";
    private static final String DEFAULT_KEYSTONE_DOMAIN = "Default";
    private static final String USERNAME_DOMAIN_SEPARATOR = "@";
    private static final String FATAL_ERROR_BASIC_AUTH_ONLY = "{\"error\":\"Only basic authentication is supported\"}";
    private static final String FATAL_ERROR_INVALID_URL = "{\"error\":\"Invalid URL to Keystone server\"}";
    private static final String UNABLE_TO_AUTHENTICATE = "{\"error\":\"Could not authenticate\"}";
    private static final String AUTH_PATH = "v3/auth/tokens";

    private static final int CLIENT_EXPIRE_AFTER_ACCESS = 1;
    private static final int CLIENT_EXPIRE_AFTER_WRITE = 10;

    private volatile URI serverUri = null;
    private volatile boolean sslVerification = true;
    private volatile String defaultDomain = DEFAULT_KEYSTONE_DOMAIN;

    private final LoadingCache clientCache = buildCache();

    private final ICertificateManager certManager;

    public KeystoneAuthRealm() {
        this.certManager = Objects.requireNonNull(ThreadLocals.CERT_MANAGER_TL.get());
        LOG.info("KeystoneAuthRealm created");
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principalCollection) {
        final Object primaryPrincipal = getAvailablePrincipal(principalCollection);
        final ODLPrincipal odlPrincipal;
        try {
            odlPrincipal = (ODLPrincipal) primaryPrincipal;
            return new SimpleAuthorizationInfo(odlPrincipal.getRoles());
        } catch (ClassCastException e) {
            LOG.error("Couldn't decode authorization request", e);
        }
        return new SimpleAuthorizationInfo();
    }

    @Override
    @SuppressWarnings("checkstyle:AvoidHidingCauseException")
    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken authenticationToken) {
        try {
            final boolean hasSslVerification = getSslVerification();
            final SimpleHttpClient client = clientCache.getUnchecked(hasSslVerification);
            return doGetAuthenticationInfo(authenticationToken, client);
        } catch (UncheckedExecutionException e) {
            Throwable cause = e.getCause();
            if (!Objects.isNull(cause) && cause instanceof AuthenticationException) {
                throw (AuthenticationException) cause;
            }
            throw e;
        }
    }

    /**
     * As {@link #doGetAuthenticationInfo(AuthenticationToken)}
     * but using the provided {@link SimpleHttpClient} to reach
     * the Keystone server.
     *
     * @param authenticationToken see
     *  {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
     * @param client the {@link SimpleHttpClient} to use.
     * @return see
     *  {@link AuthorizingRealm#doGetAuthenticationInfo(AuthenticationToken)}
     */
    protected AuthenticationInfo doGetAuthenticationInfo(
            final AuthenticationToken authenticationToken,
            final SimpleHttpClient client) {

        final URI theServerUri = getServerUri();
        final String theDefaultDomain = getDefaultDomain();

        if (!(authenticationToken instanceof UsernamePasswordToken)) {
            LOG.error("Only basic authentication is supported");
            throw new AuthenticationException(FATAL_ERROR_BASIC_AUTH_ONLY);
        }

        if (Objects.isNull(theServerUri)) {
            LOG.error("Invalid URL to Keystone server");
            throw new AuthenticationException(FATAL_ERROR_INVALID_URL);
        }

        final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        final String qualifiedUser = usernamePasswordToken.getUsername();
        final String password = new String(usernamePasswordToken.getPassword());
        final String[] qualifiedUserArray = qualifiedUser.split(USERNAME_DOMAIN_SEPARATOR, 2);
        final String username = qualifiedUserArray.length > 0 ? qualifiedUserArray[0] : qualifiedUser;
        final String domain = qualifiedUserArray.length > 1 ? qualifiedUserArray[1] : theDefaultDomain;

        final KeystoneAuth keystoneAuth = new KeystoneAuth(username, password, domain);
        final SimpleHttpRequest httpRequest = client.requestBuilder(KeystoneToken.class)
                .uri(theServerUri)
                .path(AUTH_PATH)
                .method(HttpMethod.POST)
                .mediaType(MediaType.APPLICATION_JSON_TYPE)
                .entity(keystoneAuth)
                .queryParam(NO_CATALOG_OPTION,"")
                .build();

        KeystoneToken theToken;
        try {
            theToken = httpRequest.execute();
        } catch (WebApplicationException e) {
            LOG.debug("Unable to authenticate - Keystone result code: {}", e.getResponse().getStatus(), e);
            return null;
        }

        final Set theRoles = theToken.getToken().getRoles()
                .stream()
                .map(KeystoneToken.Token.Role::getName)
                .collect(Collectors.toSet());

        final String userId = username + USERNAME_DOMAIN_SEPARATOR + domain;
        final ODLPrincipal odlPrincipal = ODLPrincipalImpl.createODLPrincipal(username, domain, userId, theRoles);
        return new SimpleAuthenticationInfo(odlPrincipal, password.toCharArray(), getName());
    }

    /**
     * Used to build a cache of {@link SimpleHttpClient}. In practice, only one
     * client instance is used at a time but SSL verification flag is used as
     * key for convenience.
     *
     * @return the cache.
     */
    protected LoadingCache buildCache() {
        return CacheBuilder.newBuilder()
                .expireAfterAccess(CLIENT_EXPIRE_AFTER_ACCESS, TimeUnit.SECONDS)
                .expireAfterWrite(CLIENT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
                .build(new CacheLoader() {
                    @Override
                    public SimpleHttpClient load(Boolean withSslVerification) throws Exception {
                        return buildClient(withSslVerification, certManager, SimpleHttpClient.clientBuilder());
                    }
                });
    }

    /**
     * Used to obtain a {@link SimpleHttpClient} that optionally performs SSL
     * verification.
     *
     * @param withSslVerification if client should perform SSL verification.
     * @param certificateManager used to obtain a secure SSL context.
     * @param clientBuilder uset to build {@link SimpleHttpClient}.
     * @return the {@link SimpleHttpClient}.
     */
    protected SimpleHttpClient buildClient(
            final boolean withSslVerification,
            final ICertificateManager certificateManager,
            final SimpleHttpClient.Builder clientBuilder) {
        final SSLContext sslContext;
        final HostnameVerifier hostnameVerifier;
        if (withSslVerification) {
            sslContext = getSecureSSLContext(certificateManager);
            hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
        } else {
            sslContext = UntrustedSSL.getSSLContext();
            hostnameVerifier = UntrustedSSL.getHostnameVerifier();
        }
        return clientBuilder
                .hostnameVerifier(hostnameVerifier)
                .sslContext(sslContext)
                .provider(GsonProvider.class)
                .build();
    }

    private SSLContext getSecureSSLContext(final ICertificateManager certificateManager) {
        final SSLContext sslContext = Optional.ofNullable(certificateManager)
                .map(ICertificateManager::getServerContext)
                .orElse(null);
        if (Objects.isNull(sslContext)) {
            LOG.error("Could not get a valid SSL context from certificate manager");
            throw new AuthenticationException(UNABLE_TO_AUTHENTICATE);
        }
        return sslContext;
    }

    /**
     * The URI of the Keystone server.
     *
     * @return the URI.
     */
    public URI getServerUri() {
        return serverUri;
    }

    /**
     * Whether SSL verification is performed or untrusted access is allowed.
     *
     * @return the SSL verification flag.
     */
    public boolean getSslVerification() {
        return sslVerification;
    }

    /**
     * Default domain to use when no domain is provided within the user
     * credentials.
     *
     * @return the default domain.
     */
    public String getDefaultDomain() {
        return defaultDomain;
    }

    /**
     * The URL of the Keystone server. Injected from
     * shiro.ini.
     *
     * @param url the URL specified in shiro.ini.
     */
    public void setUrl(final String url) {
        try {
            serverUri = new URL(url).toURI();
        } catch (final MalformedURLException | URISyntaxException e) {
            LOG.error("The keystone server URL {} could not be correctly parsed", url, e);
            serverUri = null;
        }
    }

    /**
     * Whether SSL verification is performed or untrusted access is allowed.
     * Injected from shiro.ini.
     *
     * @param sslVerification specified in shiro.ini
     */
    public void setSslVerification(final boolean sslVerification) {
        this.sslVerification = sslVerification;
    }

    /**
     * Default domain to use when no domain is provided within the user
     * credentials. Injected from shiro.ini.
     *
     * @param defaultDomain specified in shiro.ini
     */
    public void setDefaultDomain(final String defaultDomain) {
        this.defaultDomain = defaultDomain;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy