fr.xebia.cloud.amazon.aws.iam.AmazonAwsIamAccountCreator.java Source code

Java tutorial

Introduction

Here is the source code for fr.xebia.cloud.amazon.aws.iam.AmazonAwsIamAccountCreator.java

Source

/*
 * Copyright 2008-2010 Xebia and the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fr.xebia.cloud.amazon.aws.iam;

import java.io.InputStream;
import java.math.BigInteger;
import java.net.URL;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.security.auth.x500.X500Principal;

import com.google.common.base.*;
import org.apache.commons.lang.RandomStringUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.x509.X509V1CertificateGenerator;
import org.jclouds.crypto.Pems;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.CreateKeyPairRequest;
import com.amazonaws.services.ec2.model.DescribeKeyPairsRequest;
import com.amazonaws.services.ec2.model.KeyPair;
import com.amazonaws.services.ec2.model.KeyPairInfo;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagement;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.amazonaws.services.identitymanagement.model.AccessKey;
import com.amazonaws.services.identitymanagement.model.AccessKeyMetadata;
import com.amazonaws.services.identitymanagement.model.AddUserToGroupRequest;
import com.amazonaws.services.identitymanagement.model.CreateAccessKeyRequest;
import com.amazonaws.services.identitymanagement.model.CreateLoginProfileRequest;
import com.amazonaws.services.identitymanagement.model.CreateUserRequest;
import com.amazonaws.services.identitymanagement.model.GetGroupRequest;
import com.amazonaws.services.identitymanagement.model.GetGroupResult;
import com.amazonaws.services.identitymanagement.model.GetLoginProfileRequest;
import com.amazonaws.services.identitymanagement.model.GetUserRequest;
import com.amazonaws.services.identitymanagement.model.Group;
import com.amazonaws.services.identitymanagement.model.ListAccessKeysRequest;
import com.amazonaws.services.identitymanagement.model.ListAccessKeysResult;
import com.amazonaws.services.identitymanagement.model.ListSigningCertificatesRequest;
import com.amazonaws.services.identitymanagement.model.LoginProfile;
import com.amazonaws.services.identitymanagement.model.NoSuchEntityException;
import com.amazonaws.services.identitymanagement.model.SigningCertificate;
import com.amazonaws.services.identitymanagement.model.UploadSigningCertificateRequest;
import com.amazonaws.services.identitymanagement.model.UploadSigningCertificateResult;
import com.amazonaws.services.identitymanagement.model.User;
import com.amazonaws.services.identitymanagement.model.StatusType;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClient;
import com.amazonaws.services.simpleemail.model.Body;
import com.amazonaws.services.simpleemail.model.Content;
import com.amazonaws.services.simpleemail.model.Destination;
import com.amazonaws.services.simpleemail.model.Message;
import com.amazonaws.services.simpleemail.model.SendEmailRequest;
import com.amazonaws.services.simpleemail.model.SendEmailResult;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Resources;

import fr.xebia.cloud.cloudinit.FreemarkerUtils;

/**
 * Create Amazon IAM accounts.
 *
 * @author <a href="mailto:cyrille@cyrilleleclerc.com">Cyrille Le Clerc</a>
 */
public class AmazonAwsIamAccountCreator {

    enum Environment {
        PRODUCTION("production"), TRAINING("training");
        private final String identifier;

        Environment(String identifier) {
            this.identifier = identifier;
        }

        public String getIdentifier() {
            return identifier;
        }
    }

    private static final String BOUNCY_CASTLE_PROVIDER_NAME = "BC";

    static {
        // adds the Bouncy castle provider to java security
        Security.addProvider(new BouncyCastleProvider());
    }

    public static void main(String[] args) throws Exception {
        try {
            AmazonAwsIamAccountCreator amazonAwsIamAccountCreator = new AmazonAwsIamAccountCreator(
                    Environment.TRAINING);

            // Create users with their own ssh key
            amazonAwsIamAccountCreator.createUsers("Admins");

            // Create users with a specific ssh key (won't create individual keys)
            //amazonAwsIamAccountCreator.createUsers("Admins", "web-caching-workshop");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected final Environment environment;

    protected AmazonEC2 ec2;

    protected AmazonIdentityManagement iam;

    protected final KeyPairGenerator keyPairGenerator;

    protected final Logger logger = LoggerFactory.getLogger(getClass());

    protected Session mailSession;

    protected Transport mailTransport;

    protected InternetAddress mailFrom;

    protected AmazonSimpleEmailService ses;

    protected final Random random = new Random();

    public AmazonAwsIamAccountCreator(Environment environment) {
        this.environment = Preconditions.checkNotNull(environment);
        try {
            keyPairGenerator = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_PROVIDER_NAME);
            keyPairGenerator.initialize(1024, new SecureRandom());

            String credentialsFileName = "AwsCredentials-" + environment.getIdentifier() + ".properties";
            InputStream credentialsAsStream = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(credentialsFileName);
            Preconditions.checkNotNull(credentialsAsStream,
                    "File '/" + credentialsFileName + "' NOT found in the classpath");
            AWSCredentials awsCredentials = new PropertiesCredentials(credentialsAsStream);
            iam = new AmazonIdentityManagementClient(awsCredentials);

            ses = new AmazonSimpleEmailServiceClient(awsCredentials);

            ec2 = new AmazonEC2Client(awsCredentials);
            ec2.setEndpoint("ec2.eu-west-1.amazonaws.com");

            InputStream smtpPropertiesAsStream = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream("smtp.properties");
            Preconditions.checkNotNull(smtpPropertiesAsStream,
                    "File '/smtp.properties' NOT found in the classpath");

            final Properties smtpProperties = new Properties();
            smtpProperties.load(smtpPropertiesAsStream);

            mailSession = Session.getInstance(smtpProperties, null);
            mailTransport = mailSession.getTransport();
            if (smtpProperties.containsKey("mail.username")) {
                mailTransport.connect(smtpProperties.getProperty("mail.username"),
                        smtpProperties.getProperty("mail.password"));
            } else {
                mailTransport.connect();
            }
            try {
                mailFrom = new InternetAddress(smtpProperties.getProperty("mail.from"));
            } catch (Exception e) {
                throw new MessagingException("Exception parsing 'mail.from' from 'smtp.properties'", e);
            }

        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * <p>
     * Create an Amazon IAM account and send the details by email.
     * </p>
     * <p>
     * Created elements:
     * </p>
     * <ul>
     * <li>password to login to the management console if none exists,</li>
     * <li>accesskey if none is active,</li>
     * <li></li>
     * </ul>
     *
     * @param userName valid email used as userName of the created account.
     */
    public void createUser(@Nonnull final String userName, GetGroupResult groupDescriptor, String keyPairName)
            throws Exception {
        Preconditions.checkNotNull(userName, "Given userName can NOT be null");
        logger.info("Process user {}", userName);

        List<String> userAccountChanges = Lists.newArrayList();

        Map<String, String> templatesParams = Maps.newHashMap();
        templatesParams.put("awsCredentialsHome", "~/.aws");
        templatesParams.put("awsCommandLinesHome", "/opt/amazon-aws");

        User user;
        try {
            user = iam.getUser(new GetUserRequest().withUserName(userName)).getUser();
        } catch (NoSuchEntityException e) {
            logger.debug("User {} does not exist, create it", userName, e);
            user = iam.createUser(new CreateUserRequest(userName)).getUser();
            userAccountChanges.add("Create user");
        }

        List<BodyPart> attachments = Lists.newArrayList();

        // AWS WEB MANAGEMENT CONSOLE LOGIN & PASSWORD
        try {
            LoginProfile loginProfile = iam.getLoginProfile(new GetLoginProfileRequest(user.getUserName()))
                    .getLoginProfile();
            templatesParams.put("loginUserName", loginProfile.getUserName());
            templatesParams.put("loginPassword", "#your password has already been generated and sent to you#");

            logger.info("Login profile already exists {}", loginProfile);
        } catch (NoSuchEntityException e) {
            // manually add a number to ensure amazon policy is respected
            String password = RandomStringUtils.randomAlphanumeric(10) + random.nextInt(10);
            LoginProfile loginProfile = iam
                    .createLoginProfile(new CreateLoginProfileRequest(user.getUserName(), password))
                    .getLoginProfile();
            userAccountChanges.add("Create user.login");
            templatesParams.put("loginUserName", loginProfile.getUserName());
            templatesParams.put("loginPassword", password);
        }

        // ADD USER TO GROUP
        Group group = groupDescriptor.getGroup();
        List<User> groupMembers = groupDescriptor.getUsers();

        boolean isUserInGroup = Iterables.any(groupMembers, new Predicate<User>() {
            public boolean apply(User groupMember) {
                return userName.equals(groupMember.getUserName());
            }

            ;
        });

        if (!isUserInGroup) {
            logger.debug("Add user {} to group {}", user, group);
            iam.addUserToGroup(new AddUserToGroupRequest(group.getGroupName(), user.getUserName()));
            groupMembers.add(user);
            userAccountChanges.add("Add user to group");
        }

        // ACCESS KEY
        boolean activeAccessKeyExists = false;
        ListAccessKeysResult listAccessKeysResult = iam
                .listAccessKeys(new ListAccessKeysRequest().withUserName(user.getUserName()));
        for (AccessKeyMetadata accessKeyMetadata : listAccessKeysResult.getAccessKeyMetadata()) {
            StatusType status = StatusType.fromValue(accessKeyMetadata.getStatus());
            if (StatusType.Active.equals(status)) {
                logger.info("Access key {} ({}) is already active, don't create another one.",
                        accessKeyMetadata.getAccessKeyId(), accessKeyMetadata.getCreateDate());
                activeAccessKeyExists = true;
                templatesParams.put("accessKeyId", accessKeyMetadata.getAccessKeyId());
                templatesParams.put("accessKeySecretId",
                        "#accessKey has already been generated and the secretId has been sent to you#");

                break;
            }
        }

        if (!activeAccessKeyExists) {
            AccessKey accessKey = iam.createAccessKey(new CreateAccessKeyRequest().withUserName(user.getUserName()))
                    .getAccessKey();
            userAccountChanges.add("Create user.accessKey");
            logger.debug("Created access key {}", accessKey);
            templatesParams.put("accessKeyId", accessKey.getAccessKeyId());
            templatesParams.put("accessKeySecretId", accessKey.getSecretAccessKey());

            // email attachment: aws-credentials.txt
            {
                BodyPart awsCredentialsBodyPart = new MimeBodyPart();
                awsCredentialsBodyPart.setFileName("aws-credentials.txt");
                templatesParams.put("attachedCredentialsFileName", awsCredentialsBodyPart.getFileName());
                String awsCredentials = FreemarkerUtils.generate(templatesParams,
                        "/fr/xebia/cloud/amazon/aws/iam/aws-credentials.txt.ftl");
                awsCredentialsBodyPart.setContent(awsCredentials, "text/plain");
                attachments.add(awsCredentialsBodyPart);
            }

        }

        // SSH KEY PAIR
        if (keyPairName == null) { // If keyPairName is null, generate it from the username
            if (userName.endsWith("@xebia.fr") || userName.endsWith("@xebia.com")) {
                keyPairName = userName.substring(0, userName.indexOf("@xebia."));
            } else {
                keyPairName = userName.replace("@", "_at_").replace(".", "_dot_").replace("+", "_plus_");
            }
        }

        try {
            List<KeyPairInfo> keyPairInfos = ec2
                    .describeKeyPairs(new DescribeKeyPairsRequest().withKeyNames(keyPairName)).getKeyPairs();
            KeyPairInfo keyPairInfo = Iterables.getOnlyElement(keyPairInfos);
            logger.info("SSH key {} already exists. Don't overwrite it.", keyPairInfo.getKeyName());
            templatesParams.put("sshKeyName", keyPairInfo.getKeyName());
            templatesParams.put("sshKeyFingerprint", keyPairInfo.getKeyFingerprint());

            String sshKeyFileName = keyPairName + ".pem";
            URL sshKeyFileURL = Thread.currentThread().getContextClassLoader().getResource(sshKeyFileName);
            if (sshKeyFileURL != null) {
                logger.info("SSH Key file {} found.", sshKeyFileName);

                BodyPart keyPairBodyPart = new MimeBodyPart();
                keyPairBodyPart.setFileName(sshKeyFileName);
                templatesParams.put("attachedSshKeyFileName", keyPairBodyPart.getFileName());
                keyPairBodyPart.setContent(Resources.toString(sshKeyFileURL, Charsets.ISO_8859_1),
                        "application/x-x509-ca-cert");
                attachments.add(keyPairBodyPart);
            } else {
                logger.info("SSH Key file {} NOT found.", sshKeyFileName);
            }

        } catch (AmazonServiceException e) {
            if ("InvalidKeyPair.NotFound".equals(e.getErrorCode())) {
                // ssh key does not exist, create it
                KeyPair keyPair = ec2.createKeyPair(new CreateKeyPairRequest(keyPairName)).getKeyPair();
                userAccountChanges.add("Create ssh key");

                logger.info("Created ssh key {}", keyPair);
                templatesParams.put("sshKeyName", keyPair.getKeyName());
                templatesParams.put("sshKeyFingerprint", keyPair.getKeyFingerprint());

                BodyPart keyPairBodyPart = new MimeBodyPart();
                keyPairBodyPart.setFileName(keyPair.getKeyName() + ".pem");
                templatesParams.put("attachedSshKeyFileName", keyPairBodyPart.getFileName());
                keyPairBodyPart.setContent(keyPair.getKeyMaterial(), "application/x-x509-ca-cert");
                attachments.add(keyPairBodyPart);
            } else {
                throw e;
            }
        }

        // X509 SELF SIGNED CERTIFICATE
        Collection<SigningCertificate> certificates = iam
                .listSigningCertificates(new ListSigningCertificatesRequest().withUserName(userName))
                .getCertificates();
        // filter active certificates
        certificates = Collections2.filter(certificates, new Predicate<SigningCertificate>() {
            @Override
            public boolean apply(SigningCertificate signingCertificate) {
                return StatusType.Active.equals(StatusType.fromValue(signingCertificate.getStatus()));
            }
        });

        if (certificates.isEmpty()) {
            java.security.KeyPair x509KeyPair = keyPairGenerator.generateKeyPair();
            X509Certificate x509Certificate = generateSelfSignedX509Certificate(userName, x509KeyPair);
            String x509CertificatePem = Pems.pem(x509Certificate);

            UploadSigningCertificateResult uploadSigningCertificateResult = iam.uploadSigningCertificate( //
                    new UploadSigningCertificateRequest(x509CertificatePem).withUserName(user.getUserName()));
            SigningCertificate signingCertificate = uploadSigningCertificateResult.getCertificate();
            templatesParams.put("x509CertificateId", signingCertificate.getCertificateId());
            userAccountChanges.add("Create x509 certificate");

            logger.info("Created x509 certificate {}", signingCertificate);

            // email attachment: x509 private key
            {
                BodyPart x509PrivateKeyBodyPart = new MimeBodyPart();
                x509PrivateKeyBodyPart.setFileName("pk-" + signingCertificate.getCertificateId() + ".pem");
                templatesParams.put("attachedX509PrivateKeyFileName", x509PrivateKeyBodyPart.getFileName());
                String x509privateKeyPem = Pems.pem(x509KeyPair.getPrivate());
                x509PrivateKeyBodyPart.setContent(x509privateKeyPem, "application/x-x509-ca-cert");
                attachments.add(x509PrivateKeyBodyPart);
            }
            // email attachment: x509 certifiate pem
            {
                BodyPart x509CertificateBodyPart = new MimeBodyPart();
                x509CertificateBodyPart.setFileName("cert-" + signingCertificate.getCertificateId() + ".pem");
                templatesParams.put("attachedX509CertificateFileName", x509CertificateBodyPart.getFileName());
                x509CertificateBodyPart.setContent(x509CertificatePem, "application/x-x509-ca-cert");
                attachments.add(x509CertificateBodyPart);
            }

        } else {
            SigningCertificate signingCertificate = Iterables.getFirst(certificates, null);
            logger.info("X509 certificate {} already exists", signingCertificate.getCertificateId());
            templatesParams.put("x509CertificateId", signingCertificate.getCertificateId());
        }

        sendEmail(templatesParams, attachments, userName);
    }

    public void createUsers(String groupName) {
        createUsers(groupName, null);
    }

    public void createUsers(String groupName, String keyPairName) {

        GetGroupResult groupDescriptor = iam.getGroup(new GetGroupRequest(groupName));

        URL emailsToVerifyURL = Thread.currentThread().getContextClassLoader()
                .getResource("accounts-to-create.txt");
        Preconditions.checkNotNull(emailsToVerifyURL, "File 'accounts-to-create.txt' NOT found in the classpath");
        Collection<String> userNames;
        try {
            userNames = Sets.newTreeSet(Resources.readLines(emailsToVerifyURL, Charsets.ISO_8859_1));
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
        userNames = Collections2.filter(userNames, new Predicate<String>() {
            @Override
            public boolean apply(@Nullable String s) {
                return !Strings.isNullOrEmpty(s);
            }
        });
        for (String userName : userNames) {
            try {
                createUser(userName, groupDescriptor, keyPairName);
            } catch (Exception e) {
                logger.error("Failure to create user '{}'", userName, e);
            }

            // sleep 10 seconds to prevent "Throttling exception"
            try {
                Thread.sleep(10 * 1000);
            } catch (InterruptedException e) {
                throw Throwables.propagate(e);
            }
        }
    }

    /**
     * Generates a self signed x509 certificate identified by the given
     * <code>userName</code> and the given <code>keyPair</code>.
     *
     * @param userName common name of {@link X500Principal} ("CN={userName}") used as
     *                 subjectDN and issuerDN.
     * @param keyPair  used for the certificate public and private key
     * @return self signed X509 certificate
     */
    @SuppressWarnings("deprecation")
    @Nonnull
    public X509Certificate generateSelfSignedX509Certificate(@Nonnull String userName,
            @Nonnull java.security.KeyPair keyPair) {
        try {
            DateTime startDate = new DateTime().minusDays(1);
            DateTime expiryDate = new DateTime().plusYears(2);

            X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
            X500Principal dnName = new X500Principal("CN=" + userName);

            certGen.setSubjectDN(dnName);
            // same as subject : self signed certificate
            certGen.setIssuerDN(dnName);
            certGen.setNotBefore(startDate.toDate());
            certGen.setNotAfter(expiryDate.toDate());
            certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
            certGen.setPublicKey(keyPair.getPublic());
            certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");

            return certGen.generate(keyPair.getPrivate(), BOUNCY_CASTLE_PROVIDER_NAME);

        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    private void sendEmail(Map<String, String> templatesParams, List<BodyPart> attachments, String toAddress)
            throws MessagingException {

        MimeBodyPart htmlAndPlainTextAlternativeBody = new MimeBodyPart();

        // TEXT AND HTML MESSAGE (gmail requires plain text alternative, otherwise, it displays the 1st plain text attachment in the preview)
        MimeMultipart cover = new MimeMultipart("alternative");
        htmlAndPlainTextAlternativeBody.setContent(cover);
        BodyPart textHtmlBodyPart = new MimeBodyPart();
        String textHtmlBody = FreemarkerUtils.generate(templatesParams,
                "/fr/xebia/cloud/amazon/aws/iam/amazon-aws-iam-credentials-email-" + environment.getIdentifier()
                        + ".html.ftl");
        textHtmlBodyPart.setContent(textHtmlBody, "text/html");
        cover.addBodyPart(textHtmlBodyPart);

        BodyPart textPlainBodyPart = new MimeBodyPart();
        cover.addBodyPart(textPlainBodyPart);
        String textPlainBody = FreemarkerUtils.generate(templatesParams,
                "/fr/xebia/cloud/amazon/aws/iam/amazon-aws-iam-credentials-email-" + environment.getIdentifier()
                        + ".txt.ftl");
        textPlainBodyPart.setContent(textPlainBody, "text/plain");

        MimeMultipart content = new MimeMultipart("related");
        content.addBodyPart(htmlAndPlainTextAlternativeBody);

        // ATTACHMENTS
        for (BodyPart bodyPart : attachments) {
            content.addBodyPart(bodyPart);
        }

        MimeMessage msg = new MimeMessage(mailSession);

        msg.setFrom(mailFrom);
        msg.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(toAddress));
        msg.addRecipient(javax.mail.Message.RecipientType.CC, mailFrom);

        String subject = "[Xebia Amazon AWS " + environment.getIdentifier() + "] Credentials";

        msg.setSubject(subject);
        msg.setContent(content);

        mailTransport.sendMessage(msg, msg.getAllRecipients());
    }

    /**
     * Send email with Amazon Simple Email Service.
     * <p/>
     * <p/>
     * Please note that the sender (ie 'from') must be a verified address (see
     * {@link AmazonSimpleEmailService#verifyEmailAddress(com.amazonaws.services.simpleemail.model.VerifyEmailAddressRequest)}
     * ).
     * <p/>
     * <p/>
     * Please note that the sender is a CC of the meail to ease support.
     * <p/>
     *
     * @param subject
     * @param body
     * @param from
     * @param toAddresses
     */

    public void sendEmail(String subject, String body, String from, String... toAddresses) {

        SendEmailRequest sendEmailRequest = new SendEmailRequest( //
                from, //
                new Destination().withToAddresses(toAddresses).withCcAddresses(from), //
                new Message(new Content(subject), //
                        new Body(new Content(body))));
        SendEmailResult sendEmailResult = ses.sendEmail(sendEmailRequest);
        System.out.println(sendEmailResult);
    }

}