/************************************************************************* * 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( ) ) ) ) ); } } } }