1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package software.amazon.awssdk.profiles; 17 18 import java.io.InputStream; 19 import java.nio.file.Files; 20 import java.nio.file.Path; 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.HashMap; 24 import java.util.LinkedHashMap; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Map.Entry; 28 import java.util.Objects; 29 import java.util.Optional; 30 import software.amazon.awssdk.annotations.SdkPublicApi; 31 import software.amazon.awssdk.profiles.internal.ProfileFileReader; 32 import software.amazon.awssdk.utils.FunctionalUtils; 33 import software.amazon.awssdk.utils.IoUtils; 34 import software.amazon.awssdk.utils.ToString; 35 import software.amazon.awssdk.utils.Validate; 36 import software.amazon.awssdk.utils.builder.SdkBuilder; 37 38 /** 39 * Provides programmatic access to the contents of an AWS configuration profile file. 40 * 41 * AWS configuration profiles allow you to share multiple sets of AWS security credentials between different tools such as the 42 * AWS SDK for Java and the AWS CLI. 43 * 44 * <p> 45 * For more information on setting up AWS configuration profiles, see: 46 * http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html 47 * 48 * <p> 49 * A profile file can be created with {@link #builder()} and merged with other profiles files with {@link #aggregator()}. By 50 * default, the SDK will use the {@link #defaultProfileFile()} when that behavior hasn't been explicitly overridden. 51 */ 52 @SdkPublicApi 53 public final class ProfileFile { 54 public static final String PROFILES_SECTION_TITLE = "profiles"; 55 private final Map<String, Map<String, Profile>> profilesAndSectionsMap; 56 57 /** 58 * @see #builder() 59 */ ProfileFile(Map<String, Map<String, Map<String, String>>> profilesSectionMap)60 private ProfileFile(Map<String, Map<String, Map<String, String>>> profilesSectionMap) { 61 Validate.paramNotNull(profilesSectionMap, "profilesSectionMap"); 62 this.profilesAndSectionsMap = convertToProfilesSectionsMap(profilesSectionMap); 63 } 64 getSection(String sectionName, String sectionTitle)65 public Optional<Profile> getSection(String sectionName, String sectionTitle) { 66 Map<String, Profile> sectionMap = profilesAndSectionsMap.get(sectionName); 67 if (sectionMap != null) { 68 return Optional.ofNullable(sectionMap.get(sectionTitle)); 69 } 70 return Optional.empty(); 71 } 72 73 /** 74 * Create a builder for a {@link ProfileFile}. 75 */ builder()76 public static Builder builder() { 77 return new BuilderImpl(); 78 } 79 80 /** 81 * Create a builder that can merge multiple {@link ProfileFile}s together. 82 */ aggregator()83 public static Aggregator aggregator() { 84 return new Aggregator(); 85 } 86 87 /** 88 * Get the default profile file, using the credentials file from "~/.aws/credentials", the config file from "~/.aws/config" 89 * and the "default" profile. This default behavior can be customized using the 90 * {@link ProfileFileSystemSetting#AWS_SHARED_CREDENTIALS_FILE}, {@link ProfileFileSystemSetting#AWS_CONFIG_FILE} and 91 * {@link ProfileFileSystemSetting#AWS_PROFILE} settings or by specifying a different profile file and profile name. 92 * 93 * <p> 94 * The file is read each time this method is invoked. 95 */ defaultProfileFile()96 public static ProfileFile defaultProfileFile() { 97 return ProfileFile.aggregator() 98 .applyMutation(ProfileFile::addCredentialsFile) 99 .applyMutation(ProfileFile::addConfigFile) 100 .build(); 101 } 102 103 /** 104 * Retrieve the profile from this file with the given name. 105 * 106 * @param profileName The name of the profile that should be retrieved from this file. 107 * @return The profile, if available. 108 */ profile(String profileName)109 public Optional<Profile> profile(String profileName) { 110 Map<String, Profile> profileMap = profilesAndSectionsMap.get(PROFILES_SECTION_TITLE); 111 return profileMap != null ? Optional.ofNullable(profileMap.get(profileName)) : Optional.empty(); 112 } 113 114 /** 115 * Retrieve an unmodifiable collection including all of the profiles in this file. 116 * @return An unmodifiable collection of the profiles in this file, keyed by profile name. 117 */ profiles()118 public Map<String, Profile> profiles() { 119 Map<String, Profile> profileMap = profilesAndSectionsMap.get(PROFILES_SECTION_TITLE); 120 return profileMap != null ? Collections.unmodifiableMap(profileMap) : Collections.emptyMap(); 121 } 122 123 @Override toString()124 public String toString() { 125 Map<String, Profile> profiles = profilesAndSectionsMap.get(PROFILES_SECTION_TITLE); 126 return ToString.builder("ProfileFile") 127 .add("sections", profilesAndSectionsMap.keySet()) 128 .add("profiles", profiles == null ? null : profiles.values()) 129 .build(); 130 } 131 132 @Override equals(Object o)133 public boolean equals(Object o) { 134 if (this == o) { 135 return true; 136 } 137 if (o == null || getClass() != o.getClass()) { 138 return false; 139 } 140 ProfileFile that = (ProfileFile) o; 141 return Objects.equals(profilesAndSectionsMap, that.profilesAndSectionsMap); 142 } 143 144 @Override hashCode()145 public int hashCode() { 146 return Objects.hashCode(this.profilesAndSectionsMap); 147 } 148 addCredentialsFile(ProfileFile.Aggregator builder)149 private static void addCredentialsFile(ProfileFile.Aggregator builder) { 150 ProfileFileLocation.credentialsFileLocation() 151 .ifPresent(l -> builder.addFile(ProfileFile.builder() 152 .content(l) 153 .type(ProfileFile.Type.CREDENTIALS) 154 .build())); 155 } 156 addConfigFile(ProfileFile.Aggregator builder)157 private static void addConfigFile(ProfileFile.Aggregator builder) { 158 ProfileFileLocation.configurationFileLocation() 159 .ifPresent(l -> builder.addFile(ProfileFile.builder() 160 .content(l) 161 .type(ProfileFile.Type.CONFIGURATION) 162 .build())); 163 } 164 165 /** 166 * Convert the sorted map of profile and section properties into a sorted list of profiles and sections. 167 * Example: sortedProfilesSectionMap 168 * @param sortedProfilesSectionMap : Map of String to Profile/Sessions defined. 169 * <pre> 170 * {@code 171 * [profile sso-token] 172 * sso_session = admin 173 * [sso-session admin] 174 * sso_start_url = https://view.awsapps.com/start 175 * } 176 * </pre> would look like 177 * <pre> 178 * {@code 179 * sortedProfilesSectionMap 180 * profiles -- // Profile Section Title 181 * sso-token -- // Profile Name 182 * sso_session = admin // Property definition 183 * sso-session -- // Section title for Sso-sessions 184 * admin -- 185 * sso_start_url = https://view.awsapps.com/start 186 * 187 * } 188 * </pre> 189 * @return Map with keys representing Profiles and sections and value as Map with keys as profile/section name and value as 190 * property definition. 191 */ convertToProfilesSectionsMap( Map<String, Map<String, Map<String, String>>> sortedProfilesSectionMap)192 private Map<String, Map<String, Profile>> convertToProfilesSectionsMap( 193 Map<String, Map<String, Map<String, String>>> sortedProfilesSectionMap) { 194 195 Map<String, Map<String, Profile>> result = new LinkedHashMap<>(); 196 197 sortedProfilesSectionMap.entrySet() 198 .forEach(sections -> { 199 result.put(sections.getKey(), new LinkedHashMap<>()); 200 Map<String, Profile> stringProfileMap = result.get(sections.getKey()); 201 sections.getValue().entrySet() 202 .forEach(section -> { 203 Profile profile = Profile.builder() 204 .name(section.getKey()) 205 .properties(section.getValue()) 206 .build(); 207 stringProfileMap.put(section.getKey(), profile); 208 209 }); 210 }); 211 return result; 212 } 213 214 /** 215 * The supported types of profile files. The type of profile determines the way in which it is parsed. 216 */ 217 public enum Type { 218 /** 219 * A configuration profile file, typically located at ~/.aws/config, that expects all profile names (except the default 220 * profile) to be prefixed with "profile ". Any non-default profiles without this prefix will be ignored. 221 */ 222 CONFIGURATION, 223 224 /** 225 * A credentials profile file, typically located at ~/.aws/credentials, that expects all profile name to have no 226 * "profile " prefix. Any profiles with a profile prefix will be ignored. 227 */ 228 CREDENTIALS 229 } 230 231 /** 232 * A builder for a {@link ProfileFile}. {@link #content(Path)} (or {@link #content(InputStream)}) and {@link #type(Type)} are 233 * required fields. 234 */ 235 public interface Builder extends SdkBuilder<Builder, ProfileFile> { 236 /** 237 * Configure the content of the profile file. This stream will be read from and then closed when {@link #build()} is 238 * invoked. 239 */ content(InputStream contentStream)240 Builder content(InputStream contentStream); 241 242 /** 243 * Configure the location from which the profile file should be loaded. 244 */ content(Path contentLocation)245 Builder content(Path contentLocation); 246 247 /** 248 * Configure the {@link Type} of file that should be loaded. 249 */ type(Type type)250 Builder type(Type type); 251 252 @Override build()253 ProfileFile build(); 254 } 255 256 private static final class BuilderImpl implements Builder { 257 private InputStream content; 258 private Path contentLocation; 259 private Type type; 260 BuilderImpl()261 private BuilderImpl() { 262 } 263 264 @Override content(InputStream contentStream)265 public Builder content(InputStream contentStream) { 266 this.contentLocation = null; 267 this.content = contentStream; 268 return this; 269 } 270 setContent(InputStream contentStream)271 public void setContent(InputStream contentStream) { 272 content(contentStream); 273 } 274 275 @Override content(Path contentLocation)276 public Builder content(Path contentLocation) { 277 Validate.paramNotNull(contentLocation, "profileLocation"); 278 Validate.validState(Files.exists(contentLocation), "Profile file '%s' does not exist.", contentLocation); 279 280 this.content = null; 281 this.contentLocation = contentLocation; 282 return this; 283 } 284 setContentLocation(Path contentLocation)285 public void setContentLocation(Path contentLocation) { 286 content(contentLocation); 287 } 288 289 /** 290 * Configure the {@link Type} of file that should be loaded. 291 */ 292 @Override type(Type type)293 public Builder type(Type type) { 294 this.type = type; 295 return this; 296 } 297 setType(Type type)298 public void setType(Type type) { 299 type(type); 300 } 301 302 @Override build()303 public ProfileFile build() { 304 InputStream stream = content != null ? content : 305 FunctionalUtils.invokeSafely(() -> Files.newInputStream(contentLocation)); 306 307 Validate.paramNotNull(type, "type"); 308 Validate.paramNotNull(stream, "content"); 309 310 try { 311 return new ProfileFile(ProfileFileReader.parseFile(stream, type)); 312 } finally { 313 IoUtils.closeQuietly(stream, null); 314 } 315 } 316 } 317 318 /** 319 * A mechanism for merging multiple {@link ProfileFile}s together into a single file. This will merge their profiles and 320 * properties together. 321 */ 322 public static final class Aggregator implements SdkBuilder<Aggregator, ProfileFile> { 323 private List<ProfileFile> files = new ArrayList<>(); 324 325 /** 326 * Add a file to be aggregated. In the event that there is a duplicate profile/property pair in the files, files added 327 * earliest to this aggregator will take precedence, dropping the duplicated properties in the later files. 328 */ addFile(ProfileFile file)329 public Aggregator addFile(ProfileFile file) { 330 files.add(file); 331 return this; 332 } 333 334 @Override build()335 public ProfileFile build() { 336 Map<String, Map<String, Map<String, String>>> aggregateRawProfiles = new LinkedHashMap<>(); 337 for (int i = files.size() - 1; i >= 0; --i) { 338 files.get(i).profilesAndSectionsMap.entrySet() 339 .forEach(sectionKeyValue -> addToAggregate(aggregateRawProfiles, 340 sectionKeyValue.getValue(), 341 sectionKeyValue.getKey())); 342 } 343 return new ProfileFile(aggregateRawProfiles); 344 } 345 addToAggregate(Map<String, Map<String, Map<String, String>>> aggregateRawProfiles, Map<String, Profile> profiles, String sectionName)346 private void addToAggregate(Map<String, Map<String, Map<String, String>>> aggregateRawProfiles, 347 Map<String, Profile> profiles, String sectionName) { 348 349 aggregateRawProfiles.putIfAbsent(sectionName, new LinkedHashMap<>()); 350 Map<String, Map<String, String>> profileMap = aggregateRawProfiles.get(sectionName); 351 for (Entry<String, Profile> profile : profiles.entrySet()) { 352 profileMap.compute(profile.getKey(), (k, current) -> { 353 if (current == null) { 354 return new HashMap<>(profile.getValue().properties()); 355 } else { 356 current.putAll(profile.getValue().properties()); 357 return current; 358 } 359 }); 360 } 361 } 362 } 363 } 364