/************************************************************************* * 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.tokens.ws; import static com.eucalyptus.auth.principal.TemporaryAccessKey.TemporaryKeyType; import java.util.EnumSet; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.security.auth.Subject; import javax.security.auth.login.CredentialExpiredException; import javax.security.auth.login.LoginException; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import com.eucalyptus.auth.euare.DelegatingUserPrincipal; import com.eucalyptus.auth.login.AccountUsernamePasswordCredentials; import com.eucalyptus.auth.login.SecurityContext; import com.eucalyptus.auth.principal.PolicyVersion; import com.eucalyptus.auth.principal.PolicyVersions; import com.eucalyptus.auth.principal.Principals; import com.eucalyptus.auth.principal.UserPrincipal; import com.eucalyptus.component.annotation.ComponentPart; import com.eucalyptus.component.id.Tokens; import com.eucalyptus.context.Context; import com.eucalyptus.context.Contexts; import com.eucalyptus.crypto.util.B64; import com.eucalyptus.crypto.util.SecurityParameter; import com.eucalyptus.http.MappingHttpRequest; import com.eucalyptus.ws.handlers.MessageStackHandler; import com.eucalyptus.crypto.util.SecurityHeader; import com.eucalyptus.ws.protocol.OperationParameter; import com.eucalyptus.ws.server.NioServerHandler; import com.eucalyptus.ws.server.QueryPipeline; import com.eucalyptus.ws.stages.UnrollableStage; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; /** * */ @ComponentPart( Tokens.class ) public class TokensQueryPipeline extends QueryPipeline { private final TokensAuthenticationStage auth = new TokensAuthenticationStage( super.getAuthenticationStage() ); public TokensQueryPipeline() { super( "tokens-query-pipeline", "/services/Tokens", EnumSet.of( TemporaryKeyType.Access, TemporaryKeyType.Role ) ); } @Override public ChannelPipeline addHandlers( final ChannelPipeline pipeline ) { super.addHandlers( pipeline ); pipeline.addLast( "tokens-query-binding", new TokensQueryBinding( ) ); return pipeline; } @Override protected UnrollableStage getAuthenticationStage() { return auth; } private static class TokensAuthenticationStage implements UnrollableStage { private final UnrollableStage standardAuthenticationStage; private TokensAuthenticationStage(final UnrollableStage standardAuthenticationStage) { this.standardAuthenticationStage = standardAuthenticationStage; } @Override public void unrollStage( final ChannelPipeline pipeline ) { pipeline.addLast( "tokens-authentication-method-selector", new TokensAuthenticationHandler( standardAuthenticationStage ) ); } @Override public String getName() { return "tokens-user-authentication"; } @Override public int compareTo( UnrollableStage o ) { return this.getName( ).compareTo( o.getName( ) ); } } public static class TokensAuthenticationHandler extends MessageStackHandler { private final UnrollableStage standardAuthenticationStage; public TokensAuthenticationHandler(final UnrollableStage standardAuthenticationStage) { this.standardAuthenticationStage = standardAuthenticationStage; } @Override public void incomingMessage( final MessageEvent event ) throws Exception { boolean usePasswordAuth = false; boolean isAnonymous = false; if ( event.getMessage( ) instanceof MappingHttpRequest) { final MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( ); isAnonymous = "AssumeRoleWithWebIdentity".equals( OperationParameter.getParameter( httpRequest.getParameters( ) ) ); usePasswordAuth = !httpRequest.getParameters().containsKey( SecurityParameter.SignatureVersion.parameter() ) && !httpRequest.getParameters().containsKey( SecurityParameter.X_Amz_Algorithm.parameter() ) && !SecurityHeader.Value.AWS4_HMAC_SHA256.matches( httpRequest.getHeader( HttpHeaders.Names.AUTHORIZATION ) ) ; } final ChannelPipeline stagePipeline = Channels.pipeline(); if ( isAnonymous ) { stagePipeline.addLast( "tokens-anonymous", new AnonymousRequestHandler( ) ); } else if ( usePasswordAuth ) { stagePipeline.addLast( "tokens-password-authentication", new AccountUsernamePasswordHandler() ); } else { standardAuthenticationStage.unrollStage( stagePipeline ); } final ChannelPipeline pipeline = event.getChannel().getPipeline(); String addAfter = "tokens-authentication-method-selector"; for ( final Map.Entry<String,ChannelHandler> handlerEntry : stagePipeline.toMap().entrySet() ) { pipeline.addAfter( addAfter, handlerEntry.getKey(), handlerEntry.getValue() ); addAfter = handlerEntry.getKey(); } } } public static class AnonymousRequestHandler extends MessageStackHandler { @Override public void incomingMessage( final MessageEvent event ) throws Exception { if ( event.getMessage( ) instanceof MappingHttpRequest ) { final MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( ); final Context context = Contexts.lookup( httpRequest.getCorrelationId( ) ); final Subject subject = new Subject( ); final UserPrincipal principal = new DelegatingUserPrincipal( Principals.nobodyUser( ) ) { @Nonnull @Override public List<PolicyVersion> getPrincipalPolicies( ) { return ImmutableList.of( PolicyVersions.getAdministratorPolicy( ) ); } }; subject.getPrincipals( ).add( principal ); context.setUser( principal ); context.setSubject( subject ); } } } public static class AccountUsernamePasswordHandler extends MessageStackHandler { private static class ChallengeException extends Exception { private static final long serialVersionUID = 1L; } @Override public void handleUpstream(final ChannelHandlerContext ctx, final ChannelEvent channelEvent) throws Exception { try { super.handleUpstream(ctx, channelEvent); } catch ( final ChallengeException e ) { sendResponse( channelEvent, "Unauthorized", HttpResponseStatus.UNAUTHORIZED, true ); } catch ( final CredentialExpiredException e ) { sendResponse( channelEvent, "Expired credentials", HttpResponseStatus.FORBIDDEN, false ); } } @Override public void incomingMessage( final MessageEvent event ) throws Exception { if ( event.getMessage( ) instanceof MappingHttpRequest) { final MappingHttpRequest httpRequest = ( MappingHttpRequest ) event.getMessage( ); boolean challenge = true; //AUTHORIZATION looks like: username@account:password //CHANGEPASSWORD looks like: username@account;password@newPassword // all fields before any delimiter are base64 encoded. The entire string is base64 encoded as well if ( httpRequest.containsHeader( HttpHeaders.Names.AUTHORIZATION ) ) { final String[] authorization = httpRequest.getHeader( HttpHeaders.Names.AUTHORIZATION ).split( " ", 2 ); // in an effort to not break basic auth when doing a password change, we'll use a ';' instead of ':' // to indicate a new password is expected as well as encoded old password if ( authorization.length==2 && "basic".equalsIgnoreCase(authorization[0]) ) { final String unEncodedAuth = B64.standard.decString( authorization[1] ); final boolean isChangePassword = isChangePassword( unEncodedAuth ); final String[] basicUsernamePassword = unEncodedAuth.split( (isChangePassword?";":":"), 2 ); final String[] encodedAccountUsername = basicUsernamePassword[0].split( "@" , 2 ); if ( basicUsernamePassword.length==2 && encodedAccountUsername.length==2 ) { final String account = B64.standard.decString( encodedAccountUsername[1] ); final String username = B64.standard.decString( encodedAccountUsername[0] ); final String passwordSubstring = basicUsernamePassword[1]; final String[] passwords = passwordSubstring.split( "@" , 2 ); final String password = isChangePassword ? B64.standard.decString( passwords[0] ) : passwordSubstring; final String newPassword = ( isChangePassword && passwords.length == 2 ) ? passwords[1] : null; try { SecurityContext.getLoginContext( new AccountUsernamePasswordCredentials( httpRequest.getCorrelationId( ), account, username, password, newPassword ) ).login(); challenge = false; } catch ( CredentialExpiredException e ){ throw e; } catch ( LoginException e ){ // Challenge user and try again } } } } if ( challenge ) { throw new ChallengeException(); } } } @Override public void outgoingMessage( ChannelHandlerContext ctx, MessageEvent event ) throws Exception {} private void sendResponse( final ChannelEvent channelEvent, final String message, final HttpResponseStatus status, final boolean requestAuthenticate ) { final MappingHttpRequest httpRequest = ( MappingHttpRequest )((MessageEvent)channelEvent).getMessage( ); final ChannelBuffer buffer = ChannelBuffers.wrappedBuffer( message.getBytes(Charsets.UTF_8) ); final HttpResponse response = new DefaultHttpResponse( httpRequest.getProtocolVersion( ), status ); response.setHeader( HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=utf8" ); response.setHeader( HttpHeaders.Names.CONTENT_LENGTH, Integer.toString(buffer.readableBytes()) ); if ( requestAuthenticate ) { response.setHeader( HttpHeaders.Names.WWW_AUTHENTICATE, "Basic realm=\"eucalyptus\"" ); } response.setContent(buffer); final ChannelFuture writeFuture = channelEvent.getChannel().write( response ); if ( !NioServerHandler.isPersistentConnection( httpRequest ) ) { writeFuture.addListener( ChannelFutureListener.CLOSE ); } } static boolean isChangePassword( final String unEncodedAuth ) { boolean changePassword = false; if ( ( unEncodedAuth.indexOf( ";" ) > -1 )) { if ( unEncodedAuth.indexOf(":") == -1 || !(unEncodedAuth.indexOf(":") < unEncodedAuth.indexOf(";"))) changePassword = true; } return changePassword; } } }