• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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