/*************************************************************************
* Copyright 2009-2015 Eucalyptus Systems, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
* CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
* additional information or have any questions.
************************************************************************/
package com.eucalyptus.auth.euare.identity.region;
import static com.eucalyptus.auth.RegionService.regionName;
import static com.eucalyptus.auth.RegionService.serviceType;
import static com.eucalyptus.auth.euare.identity.region.RegionInfo.RegionService;
import static com.eucalyptus.util.CollectionUtils.propertyContainsPredicate;
import static com.eucalyptus.util.CollectionUtils.propertyPredicate;
import java.net.InetAddress;
import java.net.URI;
import java.security.MessageDigest;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.eucalyptus.auth.*;
import com.eucalyptus.auth.util.Identifiers;
import com.eucalyptus.component.ComponentId;
import com.eucalyptus.component.ComponentIds;
import com.eucalyptus.component.Components;
import com.eucalyptus.component.ServiceConfiguration;
import com.eucalyptus.component.ServiceConfigurations;
import com.eucalyptus.component.groups.ApiEndpointServicesGroup;
import com.eucalyptus.crypto.Digest;
import com.eucalyptus.util.Cidr;
import com.eucalyptus.util.CollectionUtils;
import com.eucalyptus.util.NonNullFunction;
import com.eucalyptus.util.TypeMapper;
import com.eucalyptus.util.TypeMappers;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.common.net.InetAddresses;
/**
*
*/
public class RegionConfigurationManager {
private static final Supplier<Optional<RegionConfiguration>> regionConfigurationSupplier =
Suppliers.memoizeWithExpiration( new Supplier<Optional<RegionConfiguration>>( ) {
@Override
public Optional<RegionConfiguration> get( ) {
return RegionConfigurations.getRegionConfiguration( );
}
}, 1, TimeUnit.MINUTES );
private static final Cache<String,byte[]> certificateDigestCache = CacheBuilder
.<String,X509Certificate>newBuilder( )
.expireAfterWrite( 1, TimeUnit.HOURS )
.build( );
/**
* Get information for a region based on an IAM identifier (AKI, etc)
*
* @param identifier The identifier to use
* @return The region info optional
*/
@Nonnull
public Optional<RegionInfo> getRegionByIdentifier( @Nullable final String identifier ) {
Optional<RegionInfo> regionInfoOptional = Optional.absent( );
final Optional<RegionConfiguration> configurationOptional = regionConfigurationSupplier.get( );
if ( configurationOptional.isPresent( ) && identifier != null && identifier.length( ) > 5 ) {
final RegionConfiguration configuration = configurationOptional.get( );
final String regionIdPartition = identifier.substring( 3, 5 );
for ( final Region region : configuration ) {
if ( Iterables.contains(
Iterables.transform( region.getIdentifierPartitions( ), PartitionFunctions.IDENTIFIER ),
regionIdPartition ) ) {
regionInfoOptional = Optional.of( regionToRegionInfoTransform( configurationOptional ).apply( region ) );
}
}
}
return regionInfoOptional;
}
/**
* Get information for a region based on an account identifier
*
* @param accountNumber The account number to use
* @return The region info optional
*/
@Nonnull
public Optional<RegionInfo> getRegionByAccountNumber( @Nullable final String accountNumber ) {
Optional<RegionInfo> regionInfoOptional = Optional.absent( );
final Optional<RegionConfiguration> configurationOptional = regionConfigurationSupplier.get( );
if ( configurationOptional.isPresent( ) && accountNumber != null && accountNumber.length( ) == 12 ) {
final RegionConfiguration configuration = configurationOptional.get( );
final String regionIdPartition = accountNumber.substring( 0, 3 );
for ( final Region region : configuration ) {
if ( Iterables.contains(
Iterables.transform( region.getIdentifierPartitions( ), PartitionFunctions.ACCOUNT_NUMBER ),
regionIdPartition ) ) {
regionInfoOptional = Optional.of( regionToRegionInfoTransform( configurationOptional ).apply( region ) );
}
}
}
return regionInfoOptional;
}
/**
* Get the region information for the local region (if any)
*
* @return The optional region information
*/
public Optional<RegionInfo> getRegionInfo( ) {
final Optional<RegionConfiguration> regionConfigurationOptional = regionConfigurationSupplier.get();
return Iterables.tryFind(
Iterables.concat( regionConfigurationOptional.asSet() ),
propertyPredicate( RegionConfigurations.getRegionName( ).asSet(), RegionNameTransform.INSTANCE )
).transform( regionToRegionInfoTransform( regionConfigurationOptional ) );
}
/**
* Get the region information for the local region (if any)
*
* @return The optional region information
*/
public Optional<RegionInfo> getRegionInfoByHost( final String host ) {
final Optional<RegionConfiguration> regionConfigurationOptional = regionConfigurationSupplier.get( );
return Iterables.tryFind(
Iterables.concat( regionConfigurationOptional.asSet() ),
propertyContainsPredicate( host, RegionServiceHostTransform.INSTANCE )
).transform( regionToRegionInfoTransform( regionConfigurationOptional ) );
}
/**
* Get all region information (if any)
*
* @return The region information
*/
public Iterable<RegionInfo> getRegionInfos( ) {
final Optional<RegionConfiguration> regionConfigurationOptional = regionConfigurationSupplier.get( );
return Iterables.transform(
Iterables.concat( regionConfigurationOptional.asSet( ) ),
regionToRegionInfoTransform( regionConfigurationOptional ) );
}
public boolean isRegionSSLCertificate( final String host, final X509Certificate certificate ) {
boolean valid = false;
final Optional<RegionInfo> hostRegion = getRegionInfoByHost( host );
if ( hostRegion.isPresent( ) ) {
if ( hostRegion.get( ).getSslCertificateFingerprint( ) != null ) {
valid = digestMatches(
hostRegion.get( ).getSslCertificateFingerprintDigest( ),
hostRegion.get( ).getSslCertificateFingerprint( ),
certificate );
} else {
valid = digestMatches(
hostRegion.get( ).getCertificateFingerprintDigest( ),
hostRegion.get( ).getCertificateFingerprint( ),
certificate );
}
}
return valid;
}
public boolean isRegionCertificate( final X509Certificate certificate ) {
boolean found = false;
final Optional<RegionConfiguration> configurationOptional = regionConfigurationSupplier.get( );
if ( configurationOptional.isPresent( ) ) {
final RegionConfiguration configuration = configurationOptional.get( );
for ( final Region region : configuration ) {
if ( digestMatches( region.getCertificateFingerprintDigest( ), region.getCertificateFingerprint( ), certificate ) ) {
found = true;
break;
}
}
}
return found;
}
private boolean digestMatches(
final String fingerprintDigest,
final String fingerprint,
final X509Certificate certificate
) {
try {
final Digest digest = Digest.forAlgorithm( fingerprintDigest ).or( Digest.SHA256 );
final byte[] regionCertificateFingerprint = certificateDigestCache.get(
fingerprint,
new Callable<byte[]>( ) {
@Override
public byte[] call( ) throws Exception {
return BaseEncoding.base16( ).withSeparator( ":", 2 ).decode( fingerprint );
}
} );
return MessageDigest.isEqual( regionCertificateFingerprint, digest.digestBinary( certificate.getEncoded( ) ) );
} catch ( ExecutionException | CertificateEncodingException e ) {
// skip the certificate
return false;
}
}
public boolean isValidRemoteAddress( final InetAddress inetAddress ) {
return isValidAddress( inetAddress, RegionInfoToCidrSetTransform.REMOTE );
}
public boolean isValidForwardedForAddress( final String address ) {
boolean valid = false;
try {
valid = isValidAddress( InetAddresses.forString( address ), RegionInfoToCidrSetTransform.FORWARDED_FOR );
} catch ( final IllegalArgumentException e ) {
// invalid
}
return valid;
}
private boolean isValidAddress( final InetAddress inetAddress,
final NonNullFunction<RegionInfo,Set<Cidr>> cidrTransform ) {
final Optional<RegionInfo> regionInfoOptional = getRegionInfo( );
final Predicate<InetAddress> addressPredicate =
Predicates.or( Iterables.concat( regionInfoOptional.transform( cidrTransform ).asSet( ) ) );
return addressPredicate.apply( inetAddress );
}
private enum PartitionFunctions implements NonNullFunction<Integer,String> {
ACCOUNT_NUMBER {
@Nonnull
@Override
public String apply( final Integer integer ) {
return Strings.padStart( String.valueOf( integer ), 3, '0' );
}
},
IDENTIFIER {
private final char[] characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray( );
@Nonnull
@Override
public String apply( final Integer integer ) {
return new String( new char[ ]{ characters[ integer / 32 ], characters[ integer % 32 ] } );
}
},
}
private enum RegionInfoToCidrSetTransform implements NonNullFunction<RegionInfo,Set<Cidr>> {
FORWARDED_FOR {
@Nonnull
@Override
public Set<Cidr> apply( final RegionInfo regionInfo ) {
return regionInfo.getForwardedForCidrs( );
}
},
REMOTE {
@Nonnull
@Override
public Set<Cidr> apply( final RegionInfo regionInfo ) {
return regionInfo.getRemoteCidrs( );
}
},
}
private enum RegionConfigurationToCidrListTransform implements NonNullFunction<RegionConfiguration,List<String>> {
FORWARDED_FOR {
@Nonnull
@Override
public List<String> apply( final RegionConfiguration regionConfiguration ) {
final List<String> cidrs = regionConfiguration.getForwardedForCidrs( );
return cidrs == null ?
Collections.<String>emptyList( ) :
cidrs;
}
},
REMOTE {
@Nonnull
@Override
public List<String> apply( final RegionConfiguration regionConfiguration ) {
final List<String> cidrs = regionConfiguration.getRemoteCidrs( );
return cidrs == null ?
Collections.<String>emptyList( ) :
cidrs;
}
},
}
private enum RegionNameTransform implements NonNullFunction<Region,String> {
INSTANCE;
@Nonnull
@Override
public String apply( final Region region ) {
return region.getName( );
}
}
private enum RegionServiceHostTransform implements NonNullFunction<Region,Set<String>> {
INSTANCE;
@Nonnull
@Override
public Set<String> apply( final Region region ) {
final Set<String> hosts = Sets.newHashSet( );
if ( region.getServices( ) != null ) for ( final Service service : region.getServices( ) ) {
if ( service.getEndpoints( ) != null ) for ( final String uri : service.getEndpoints( ) ) {
hosts.add( URI.create( uri ).getHost( ) );
}
}
return hosts;
}
}
private enum RegionInfoPartitionsTransform implements NonNullFunction<RegionInfo,Set<Integer>> {
INSTANCE;
@Nonnull
@Override
public Set<Integer> apply(final RegionInfo region ) {
return region.getPartitions();
}
}
private static NonNullFunction<Region,RegionInfo> regionToRegionInfoTransform(
final Optional<RegionConfiguration> regionConfigurationOptional
) {
return new NonNullFunction<Region,RegionInfo>( ) {
@Nonnull
@Override
public RegionInfo apply( final Region region ) {
return new RegionInfo(
region.getName( ),
region.getIdentifierPartitions( ),
Collections2.transform( region.getServices( ), TypeMappers.lookup( Service.class, RegionService.class ) ),
buildCidrs(
regionConfigurationOptional.transform( RegionConfigurationToCidrListTransform.REMOTE ).orNull( ),
region.getRemoteCidrs( ) ),
buildCidrs(
regionConfigurationOptional.transform( RegionConfigurationToCidrListTransform.FORWARDED_FOR ).orNull( ),
region.getForwardedForCidrs( ) ),
region.getCertificateFingerprintDigest( ),
region.getCertificateFingerprint( ),
region.getSslCertificateFingerprintDigest( ),
region.getSslCertificateFingerprint( )
);
}
};
}
private static Set<Cidr> buildCidrs( final List<String> cidrList, final List<String> regionCidrList ) {
final Set<String> cidrs = Sets.newLinkedHashSet( );
if ( cidrList != null ) cidrs.addAll( cidrList );
if ( regionCidrList != null ) cidrs.addAll( regionCidrList );
if ( cidrs.isEmpty( ) ) cidrs.add( "0.0.0.0/0" );
return Sets.newLinkedHashSet( Iterables.transform( cidrs, Cidr.parseUnsafe( ) ) );
}
@TypeMapper
private enum ServiceToRegionServiceTransform implements Function<Service,RegionService> {
INSTANCE;
@Nullable
@Override
public RegionService apply( @Nullable final Service service ) {
return service == null ?
null :
new RegionService( service.getType(), service.getEndpoints() );
}
}
private enum RegionInfoToRegionServiceTransform implements NonNullFunction<RegionInfo, Iterable<com.eucalyptus.auth.RegionService>> {
INSTANCE;
@Nonnull
@Override
public Iterable<com.eucalyptus.auth.RegionService> apply( final RegionInfo region ) {
final Collection<com.eucalyptus.auth.RegionService> services = Lists.newArrayList( );
for ( final RegionService service : region.getServices( ) ) {
for ( final String endpoint : service.getEndpoints( ) ) {
services.add( new com.eucalyptus.auth.RegionService(
region.getName( ),
service.getType( ),
endpoint
) );
}
}
return services;
}
}
public static class ConfiguredIdentifierPartitionSupplier implements Identifiers.IdentifierPartitionSupplier {
private final RegionConfigurationManager regionConfigurationManager = new RegionConfigurationManager( );
@Override
public Iterable<String> getIdentifierPartitions( ) {
return Iterables.transform(
Iterables.concat(
regionConfigurationManager.getRegionInfo( ).transform( RegionInfoPartitionsTransform.INSTANCE ).asSet( ) ),
PartitionFunctions.IDENTIFIER );
}
@Override
public Iterable<String> getAccountNumberPartitions( ) {
return Iterables.transform(
Iterables.concat(
regionConfigurationManager.getRegionInfo( ).transform( RegionInfoPartitionsTransform.INSTANCE ).asSet( ) ),
PartitionFunctions.ACCOUNT_NUMBER );
}
}
public static class ConfiguredRegionProvider implements Regions.RegionProvider {
private final RegionConfigurationManager regionConfigurationManager = new RegionConfigurationManager( );
private final Supplier<RegionInfo> generatedRegionInfo = regionGeneratingSupplier( );
@Override
public List<com.eucalyptus.auth.RegionService> getRegionServicesByType( final String serviceType ) {
final Optional<RegionInfo> configuredRegionInfo = regionConfigurationManager.getRegionInfo( );
final RegionInfo localRegionInfo = configuredRegionInfo.or( generatedRegionInfo );
final Ordering<com.eucalyptus.auth.RegionService> ordering =
Ordering.natural().onResultOf( regionName( ) ).compound( Ordering.natural( ).onResultOf( serviceType( ) ) );
final NonNullFunction<RegionInfo, Iterable<com.eucalyptus.auth.RegionService>> transformer =
RegionInfoToRegionServiceTransform.INSTANCE;
final Set<com.eucalyptus.auth.RegionService> services = Sets.newTreeSet( ordering );
Iterables.addAll( services, transformer.apply( localRegionInfo ) );
Iterables.addAll( services, Iterables.concat(
Iterables.transform( regionConfigurationManager.getRegionInfos( ), transformer ) ) );
return Lists.newArrayList( Iterables.filter(
services,
CollectionUtils.propertyPredicate( serviceType, serviceType( ) ) ) );
}
private static Supplier<RegionInfo> regionGeneratingSupplier( ) {
return new Supplier<RegionInfo>( ) {
@Override
public RegionInfo get( ) {
return new RegionInfo(
RegionConfigurations.getRegionName( ).or( "eucalyptus" ),
Collections.singleton( 0 ),
Lists.newArrayList( Optional.presentInstances( Iterables.transform(
Iterables.filter( ComponentIds.list( ), ComponentIds.lookup( ApiEndpointServicesGroup.class ) ),
ComponentIdToRegionServiceTransform.INSTANCE ) ) ),
Collections.<Cidr>emptySet( ),
Collections.<Cidr>emptySet( ),
null,
null,
null,
null
);
}
};
}
private enum ComponentIdToRegionServiceTransform implements NonNullFunction<ComponentId,Optional<RegionService>> {
INSTANCE;
@Nonnull
@Override
public Optional<RegionService> apply( final ComponentId componentId ) {
final Iterable<ServiceConfiguration> serviceConfigurations = Components.lookup( componentId ).services( );
return serviceConfigurations == null || Iterables.isEmpty( serviceConfigurations ) ?
Optional.<RegionService>absent( ) :
Optional.of( new RegionService(
componentId.name( ),
Sets.newTreeSet( Iterables.transform(
serviceConfigurations,
ServiceConfigurations.remotePublicify( ) ) ) ) );
}
}
}
}