/************************************************************************* * (c) Copyright 2017 Hewlett Packard Enterprise Development Company LP * * 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/. ************************************************************************/ package com.eucalyptus.cloudformation.ws; import java.nio.charset.StandardCharsets; import java.security.Signature; import java.util.Collections; import java.util.List; import javax.security.auth.Subject; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.HttpHeaders; import com.eucalyptus.auth.Accounts; import com.eucalyptus.auth.euare.DelegatingUserPrincipal; import com.eucalyptus.auth.login.AuthenticationException; import com.eucalyptus.auth.principal.PolicyVersion; import com.eucalyptus.auth.principal.UserPrincipal; import com.eucalyptus.cloudformation.config.CloudFormationProperties; import com.eucalyptus.cloudformation.util.CfnIdentityDocumentCredential; import com.eucalyptus.component.auth.SystemCredentials; import com.eucalyptus.component.id.Eucalyptus; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.crypto.Signatures; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.util.CompatFunction; import com.eucalyptus.util.FUtils; import com.eucalyptus.util.Json; import com.eucalyptus.ws.handlers.MessageStackHandler; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilderSpec; import com.google.common.io.BaseEncoding; import javaslang.Tuple; import javaslang.Tuple2; import javaslang.control.Option; /** * */ public class CloudFormationCfnAuthenticationHandler extends MessageStackHandler { private static final String AUTH_SCHEME_CFNV1 = "CFN_V1"; private static final String DEFAULT_INSTANCE_AUTH_CACHE_SPEC = "maximumSize=1000, expireAfterWrite=5m"; private static final CompatFunction<String,Cache<Tuple2<String,String>,Option<Tuple2<String,String>>>> MEMOIZED_CACHE_BUILDER = FUtils.memoizeLast( spec -> CacheBuilder.from( CacheBuilderSpec.parse( spec ) ).build( ) ); static boolean usesCfnV1Authentication( final MappingHttpRequest request ) { final String authHeader = request.getHeader( HttpHeaders.Names.AUTHORIZATION ); return authHeader != null && authHeader.startsWith( AUTH_SCHEME_CFNV1 ); } @Override public void incomingMessage( final MessageEvent event ) throws Exception { if ( event.getMessage( ) instanceof MappingHttpRequest ) { final MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( ); final String authHeader = httpRequest.getHeader( HttpHeaders.Names.AUTHORIZATION ); if ( authHeader == null ) { throw new AuthenticationException( "Invalid or missing authorization header" ); } final int signaturePartIndex = authHeader.indexOf( ':' ); if ( signaturePartIndex < 100 ) { throw new AuthenticationException( "Invalid or missing authorization header" ); } final String instanceId; final String accountNumber; try { final String documentPart = authHeader.substring( AUTH_SCHEME_CFNV1.length( ) + 1, signaturePartIndex ); final String signaturePart = authHeader.substring( signaturePartIndex + 1 ); final byte[] documentBytes = BaseEncoding.base64( ).decode( documentPart ); final String document = new String( documentBytes, StandardCharsets.UTF_8 ); final byte[] signature = BaseEncoding.base64( ).decode( signaturePart ); final Option<Tuple2<String,String>> instanceAndAccountNumber = cache( ).get( Tuple.of( documentPart, signaturePart ), ( ) -> { final SystemCredentials.Credentials credentials = SystemCredentials.lookup( Eucalyptus.class ); final Signature sig = Signatures.SHA1WithRSA.getInstance( ); sig.initVerify( credentials.getCertificate( ) ); sig.update( documentBytes ); if ( sig.verify( signature ) ) { final ObjectNode documentObject = Json.parseObject( document ); return Option.of( Tuple.of( documentObject.get( "instanceId" ).asText( ), documentObject.get( "accountId" ).asText( ) ) ); } else { return Option.none( ); } } ); if ( instanceAndAccountNumber.isEmpty( ) ) { throw new AuthenticationException( "Invalid signature" ); } else { instanceId = instanceAndAccountNumber.get( )._1; accountNumber = instanceAndAccountNumber.get( )._2; } } catch ( IllegalArgumentException e ) { throw new AuthenticationException( "Invalid or missing authorization header" ); } // Login as account admin but without any permissions // CfnIdentityDocumentCredential identifies the requesting instance final Context context = Contexts.lookup( httpRequest.getCorrelationId( ) ); final Subject subject = new Subject( ); final UserPrincipal principal = new DelegatingUserPrincipal( Accounts.lookupCachedPrincipalByAccountNumber( accountNumber ) ) { @Override public List<PolicyVersion> getPrincipalPolicies( ) { return Collections.emptyList( ); } @Override public boolean isAccountAdmin( ) { return false; } @Override public boolean isSystemAdmin( ) { return false; } @Override public boolean isSystemUser( ) { return false; } }; subject.getPrincipals( ).add( principal ); subject.getPublicCredentials( ).add( CfnIdentityDocumentCredential.of( instanceId ) ); context.setUser( principal ); context.setSubject( subject ); } } private static Cache<Tuple2<String,String>,Option<Tuple2<String,String>>> cache( ) { return MEMOIZED_CACHE_BUILDER.apply( MoreObjects.firstNonNull( Strings.emptyToNull( CloudFormationProperties.CFN_INSTANCE_AUTH_CACHE ), DEFAULT_INSTANCE_AUTH_CACHE_SPEC ) ); } }