/* * Copyright 2014-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 com.amazonaws.auth.profile; import com.amazonaws.SdkClientException; import com.amazonaws.auth.profile.internal.AbstractProfilesConfigFileScanner; import com.amazonaws.auth.profile.internal.Profile; import com.amazonaws.auth.profile.internal.ProfileKeyConstants; import com.amazonaws.util.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Scanner; import java.util.Set; import java.util.UUID; /** * The class for creating and modifying the credential profiles file. */ public class ProfilesConfigFileWriter { private static final Log LOG = LogFactory.getLog(ProfilesConfigFileWriter.class); /** * Write all the credential profiles to a file. Note that this method will * clobber the existing content in the destination file if it's in the * overwrite mode. Use {@link #modifyOrInsertProfiles(File, Profile...)} * instead, if you want to perform in-place modification on your existing * credentials file. * * @param destination * The destination file where the credentials will be written to. * @param overwrite * If true, this method If false, this method will throw * exception if the file already exists. * @param profiles * All the credential profiles to be written. */ public static void dumpToFile(File destination, boolean overwrite, Profile... profiles) { if (destination.exists() && !overwrite) { throw new SdkClientException( "The destination file already exists. " + "Set overwrite=true if you want to clobber the existing " + "content and completely re-write the file."); } OutputStreamWriter writer; try { writer = new OutputStreamWriter(new FileOutputStream(destination, false), StringUtils.UTF8); } catch (IOException ioe) { throw new SdkClientException( "Unable to open the destination file.", ioe); } try { final Map<String, Profile> modifications = new LinkedHashMap<String, Profile>(); for (Profile profile : profiles) { modifications.put(profile.getProfileName(), profile); } ProfilesConfigFileWriterHelper writerHelper = new ProfilesConfigFileWriterHelper(writer, modifications); writerHelper.writeWithoutExistingContent(); } finally { try { writer.close(); } catch (IOException ioe) {} } } /** * Modify or insert new profiles into an existing credentials file by * in-place modification. Only the properties of the affected profiles will * be modified; all the unaffected profiles and comment lines will remain * the same. This method does not support renaming a profile. * * @param destination * The destination file to modify * @param profiles * All the credential profiles to be written. */ public static void modifyOrInsertProfiles(File destination, Profile... profiles) { final Map<String, Profile> modifications = new LinkedHashMap<String, Profile>(); for (Profile profile : profiles) { modifications.put(profile.getProfileName(), profile); } modifyProfiles(destination, modifications); } /** * Modify one profile in the existing credentials file by in-place * modification. This method will rename the existing profile if the * specified Profile has a different name. * * @param destination * The destination file to modify * @param profileName * The name of the existing profile to be modified * @param newProfile * The new Profile object. */ public static void modifyOneProfile(File destination, String profileName, Profile newProfile) { final Map<String, Profile> modifications = Collections.singletonMap(profileName, newProfile); modifyProfiles(destination, modifications); } /** * Remove one or more profiles from the existing credentials file by * in-place modification. * * @param destination * The destination file to modify * @param profileNames * The names of all the profiles to be deleted. */ public static void deleteProfiles(File destination, String... profileNames) { final Map<String, Profile> modifications = new LinkedHashMap<String, Profile>(); for (String profileName : profileNames) { modifications.put(profileName, null); // null value indicates a deletion } modifyProfiles(destination, modifications); } /** * A package-private method that supports all kinds of profile modification, * including renaming or deleting one or more profiles. * * @param modifications * Use null key value to indicate a profile that is to be * deleted. */ static void modifyProfiles(File destination, Map<String, Profile> modifications) { final boolean inPlaceModify = destination.exists(); File stashLocation = null; // Stash the original file, before we apply the changes if (inPlaceModify) { boolean stashed = false; try { // We can't use File.createTempFile, since it will always create // that file no matter what, and File.reNameTo does not allow // the destination to be an existing file stashLocation = new File(destination.getParentFile(), destination.getName() + ".bak." + UUID.randomUUID().toString()); stashed = destination.renameTo(stashLocation); if (LOG.isDebugEnabled()) { LOG.debug(String .format("The original credentials file is stashed to loaction (%s).", stashLocation.getAbsolutePath())); } } finally { if (!stashed) { throw new SdkClientException( "Failed to stash the existing credentials file " + "before applying the changes."); } } } OutputStreamWriter writer = null; try { writer = new OutputStreamWriter(new FileOutputStream(destination), StringUtils.UTF8); ProfilesConfigFileWriterHelper writerHelper = new ProfilesConfigFileWriterHelper(writer, modifications); if (inPlaceModify) { Scanner existingContent = new Scanner(stashLocation, StringUtils.UTF8.name()); writerHelper.writeWithExistingContent(existingContent); } else { writerHelper.writeWithoutExistingContent(); } // Make sure the output is valid and can be loaded by the loader new ProfilesConfigFile(destination); if ( inPlaceModify && !stashLocation.delete() ) { if (LOG.isDebugEnabled()) { LOG.debug(String .format("Successfully modified the credentials file. But failed to " + "delete the stashed copy of the original file (%s).", stashLocation.getAbsolutePath())); } } } catch (Exception e) { // Restore the stashed file if (inPlaceModify) { boolean restored = false; try { // We don't really care about what destination.delete() // returns, since the file might not have been created when // the error occurred. if ( !destination.delete() ) { LOG.debug("Unable to remove the credentials file " + "before restoring the original one."); } restored = stashLocation.renameTo(destination); } finally { if (!restored) { throw new SdkClientException( "Unable to restore the original credentials file. " + "File content stashed in " + stashLocation.getAbsolutePath()); } } } throw new SdkClientException( "Unable to modify the credentials file. " + "(The original file has been restored.)", e); } finally { try { if (writer != null) writer.close(); } catch (IOException e) {} } } /** * Implementation of AbstractProfilesConfigFileScanner, which reads the * content from an existing credentials file (if any) and then modifies some * of the profile properties in place. */ private static class ProfilesConfigFileWriterHelper extends AbstractProfilesConfigFileScanner { /** The writer where the modified profiles will be output to */ private final Writer writer; /** Map of all the profiles to be modified, keyed by profile names */ private final Map<String, Profile> newProfiles = new LinkedHashMap<String, Profile>(); /** Map of the names of all the profiles to be deleted */ private final Set<String> deletedProfiles= new HashSet<String>(); private final StringBuilder buffer = new StringBuilder(); private final Map<String, Set<String>> existingProfileProperties = new HashMap<String, Set<String>>(); /** * Creates ProfilesConfigFileWriterHelper with the specified new * profiles. * * @param writer * The writer where the modified content is output to. * @param modifications * A map of all the new profiles, keyed by the profile name. * If a profile name is associated with a null value, it's * profile content will be removed. */ public ProfilesConfigFileWriterHelper(Writer writer, Map<String, Profile> modifications) { this.writer = writer; for (Entry<String, Profile> entry : modifications.entrySet()) { String profileName = entry.getKey(); Profile profile = entry.getValue(); if (profile == null) { deletedProfiles.add(profileName); } else { newProfiles.put(profileName, profile); } } } /** * Append the new profiles to the writer, by reading from empty content. */ public void writeWithoutExistingContent() { buffer.setLength(0); existingProfileProperties.clear(); // Use empty String as input, since we are bootstrapping a new file. run(new Scanner("")); } /** * Read the existing content of a credentials file, and then make * in-place modification according to the new profiles specified in this * class. */ public void writeWithExistingContent(Scanner existingContent) { buffer.setLength(0); existingProfileProperties.clear(); run(existingContent); } @Override protected void onEmptyOrCommentLine(String profileName, String line) { /* * Buffer the line until we reach the next property line or the end * of the profile. We do this so that new properties could be * inserted at more appropriate location. For example: * * [default] * # access key * aws_access_key_id=aaa * # secret key * aws_secret_access_key=sss * # We want new properties to be inserted before this line * # instead of after the following empty line * * [next profile] * ... */ if (profileName == null || !deletedProfiles.contains(profileName)) { buffer(line); } } @Override protected void onProfileStartingLine(String profileName, String line) { existingProfileProperties.put(profileName, new HashSet<String>()); // Copy the line after flush the buffer flush(); if (deletedProfiles.contains(profileName)) return; // If the profile name is changed if (newProfiles.get(profileName) != null) { String newProfileName = newProfiles.get(profileName).getProfileName(); if ( !newProfileName.equals(profileName) ) { line = "[" + newProfileName + "]"; } } writeLine(line); } @Override protected void onProfileEndingLine(String prevProfileName) { // Check whether we need to insert new properties into this profile Profile modifiedProfile = newProfiles.get(prevProfileName); if (modifiedProfile != null) { for (Entry<String, String> entry : modifiedProfile.getProperties().entrySet()) { String propertyKey = entry.getKey(); String propertyValue = entry.getValue(); if ( !existingProfileProperties.get(prevProfileName).contains(propertyKey) ) { writeProperty(propertyKey, propertyValue); } } } // flush all the buffered comments and empty lines flush(); } @Override protected void onProfileProperty(String profileName, String propertyKey, String propertyValue, boolean isSupportedProperty, String line) { // Record that this property key has been declared for this profile if (existingProfileProperties.get(profileName) == null) { existingProfileProperties.put(profileName, new HashSet<String>()); } existingProfileProperties.get(profileName).add(propertyKey); if (deletedProfiles.contains(profileName)) return; // Keep the unsupported properties if ( !isSupportedProperty ) { writeLine(line); return; } // flush all the buffered comments and empty lines before this property line flush(); // Modify the property value if (newProfiles.containsKey(profileName)) { String newValue = newProfiles.get(profileName) .getPropertyValue(propertyKey); if (newValue != null) { writeProperty(propertyKey, newValue); } // else remove that line } else { writeLine(line); } } @Override protected void onEndOfFile() { // Append profiles that don't exist in the original file for (Entry<String, Profile> entry : newProfiles.entrySet()) { String profileName = entry.getKey(); Profile profile = entry.getValue(); if ( !existingProfileProperties.containsKey(profileName) ) { // The profile name is not found in the file // Append the profile properties writeProfile(profile); writeLine(""); } } // Flush the "real" writer try { writer.flush(); } catch (IOException ioe) { throw new SdkClientException( "Unable to write to the target file to persist the profile credentials.", ioe); } } /** * ProfilesConfigFileWriter still deals with legacy {@link Profile} interface so it can only * modify credential related properties. All other properties should be preserved when * modifying profiles. */ @Override protected boolean isSupportedProperty(String propertyName) { return ProfileKeyConstants.AWS_ACCESS_KEY_ID.equals(propertyName) || ProfileKeyConstants.AWS_SECRET_ACCESS_KEY.equals(propertyName) || ProfileKeyConstants.AWS_SESSION_TOKEN.equals(propertyName) || ProfileKeyConstants.EXTERNAL_ID.equals(propertyName) || ProfileKeyConstants.ROLE_ARN.equals(propertyName) || ProfileKeyConstants.ROLE_SESSION_NAME.equals(propertyName) || ProfileKeyConstants.SOURCE_PROFILE.equals(propertyName); } /* Private interface */ private void writeProfile(Profile profile) { writeProfileName(profile.getProfileName()); for (Entry<String, String> entry : profile.getProperties().entrySet()) { writeProperty(entry.getKey(), entry.getValue()); } } private void writeProfileName(String profileName) { writeLine(String.format("[%s]", profileName)); } private void writeProperty(String propertyKey, String propertyValue) { writeLine(String.format("%s=%s", propertyKey, propertyValue)); } private void writeLine(String line) { append(String.format("%s%n", line)); } /** * This method handles IOException that occurs when calling the append * method on the writer. */ private void append(String str) { try { writer.append(str); } catch (IOException ioe) { throw new SdkClientException( "Unable to write to the target file to persist the profile credentials.", ioe); } } private void flush() { if (buffer.length() != 0) { append(buffer.toString()); buffer.setLength(0); } } private void buffer(String line) { buffer.append(String.format("%s%n", line)); } } }