blob: 81b0336579419aaf77b9998813aaf033abc286f1 [file] [log] [blame]
/*
* Copyright 2014-2018 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));
}
}
}