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