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     private final Map<String, Profile> profiles;
55
56     /**
57      * @see #builder()
58      */

59     private ProfileFile(Map<String, Map<String, String>> rawProfiles) {
60         Validate.paramNotNull(rawProfiles, "rawProfiles");
61
62         this.profiles = Collections.unmodifiableMap(convertToProfilesMap(rawProfiles));
63     }
64
65     /**
66      * Create a builder for a {@link ProfileFile}.
67      */

68     public static Builder builder() {
69         return new BuilderImpl();
70     }
71
72     /**
73      * Create a builder that can merge multiple {@link ProfileFile}s together.
74      */

75     public static Aggregator aggregator() {
76         return new Aggregator();
77     }
78
79     /**
80      * Get the default profile file, using the credentials file from "~/.aws/credentials", the config file from "~/.aws/config"
81      * and the "default" profile. This default behavior can be customized using the
82      * {@link ProfileFileSystemSetting#AWS_SHARED_CREDENTIALS_FILE}, {@link ProfileFileSystemSetting#AWS_CONFIG_FILE} and
83      * {@link ProfileFileSystemSetting#AWS_PROFILE} settings or by specifying a different profile file and profile name.
84      *
85      * <p>
86      * The file is read each time this method is invoked.
87      */

88     public static ProfileFile defaultProfileFile() {
89         return ProfileFile.aggregator()
90                           .applyMutation(ProfileFile::addCredentialsFile)
91                           .applyMutation(ProfileFile::addConfigFile)
92                           .build();
93     }
94
95     /**
96      * Retrieve the profile from this file with the given name.
97      *
98      * @param profileName The name of the profile that should be retrieved from this file.
99      * @return The profile, if available.
100      */

101     public Optional<Profile> profile(String profileName) {
102         return Optional.ofNullable(profiles.get(profileName));
103     }
104
105     /**
106      * Retrieve an unmodifiable collection including all of the profiles in this file.
107      * @return An unmodifiable collection of the profiles in this file, keyed by profile name.
108      */

109     public Map<String, Profile> profiles() {
110         return profiles;
111     }
112
113     @Override
114     public String toString() {
115         return ToString.builder("ProfileFile")
116                        .add("profiles",  profiles.values())
117                        .build();
118     }
119
120     @Override
121     public boolean equals(Object o) {
122         if (this == o) {
123             return true;
124         }
125         if (o == null || getClass() != o.getClass()) {
126             return false;
127         }
128         ProfileFile that = (ProfileFile) o;
129         return Objects.equals(profiles, that.profiles);
130     }
131
132     @Override
133     public int hashCode() {
134         return Objects.hashCode(profiles());
135     }
136
137     private static void addCredentialsFile(ProfileFile.Aggregator builder) {
138         ProfileFileLocation.credentialsFileLocation()
139                            .ifPresent(l -> builder.addFile(ProfileFile.builder()
140                                                                       .content(l)
141                                                                       .type(ProfileFile.Type.CREDENTIALS)
142                                                                       .build()));
143     }
144
145     private static void addConfigFile(ProfileFile.Aggregator builder) {
146         ProfileFileLocation.configurationFileLocation()
147                            .ifPresent(l -> builder.addFile(ProfileFile.builder()
148                                                                       .content(l)
149                                                                       .type(ProfileFile.Type.CONFIGURATION)
150                                                                       .build()));
151     }
152
153     /**
154      * Convert the sorted map of profile properties into a sorted list of profiles.
155      */

156     private Map<String, Profile> convertToProfilesMap(Map<String, Map<String, String>> sortedProfiles) {
157         Map<String, Profile> result = new LinkedHashMap<>();
158         for (Entry<String, Map<String, String>> rawProfile : sortedProfiles.entrySet()) {
159             Profile profile = Profile.builder()
160                                      .name(rawProfile.getKey())
161                                      .properties(rawProfile.getValue())
162                                      .build();
163             result.put(profile.name(), profile);
164         }
165
166         return result;
167     }
168
169     /**
170      * The supported types of profile files. The type of profile determines the way in which it is parsed.
171      */

172     public enum Type {
173         /**
174          * A configuration profile file, typically located at ~/.aws/config, that expects all profile names (except the default
175          * profile) to be prefixed with "profile ". Any non-default profiles without this prefix will be ignored.
176          */

177         CONFIGURATION,
178
179         /**
180          * A credentials profile file, typically located at ~/.aws/credentials, that expects all profile name to have no
181          * "profile " prefix. Any profiles with a profile prefix will be ignored.
182          */

183         CREDENTIALS
184     }
185
186     /**
187      * A builder for a {@link ProfileFile}. {@link #content(Path)} (or {@link #content(InputStream)}) and {@link #type(Type)} are
188      * required fields.
189      */

190     public interface Builder extends SdkBuilder<Builder, ProfileFile> {
191         /**
192          * Configure the content of the profile file. This stream will be read from and then closed when {@link #build()} is
193          * invoked.
194          */

195         Builder content(InputStream contentStream);
196
197         /**
198          * Configure the location from which the profile file should be loaded.
199          */

200         Builder content(Path contentLocation);
201
202         /**
203          * Configure the {@link Type} of file that should be loaded.
204          */

205         Builder type(Type type);
206
207         @Override
208         ProfileFile build();
209     }
210
211     private static final class BuilderImpl implements Builder {
212         private InputStream content;
213         private Path contentLocation;
214         private Type type;
215
216         private BuilderImpl() {
217         }
218
219         @Override
220         public Builder content(InputStream contentStream) {
221             this.contentLocation = null;
222             this.content = contentStream;
223             return this;
224         }
225
226         public void setContent(InputStream contentStream) {
227             content(contentStream);
228         }
229
230         @Override
231         public Builder content(Path contentLocation) {
232             Validate.paramNotNull(contentLocation, "profileLocation");
233             Validate.validState(contentLocation.toFile().exists(), "Profile file '%s' does not exist.", contentLocation);
234
235             this.content = null;
236             this.contentLocation = contentLocation;
237             return this;
238         }
239
240         public void setContentLocation(Path contentLocation) {
241             content(contentLocation);
242         }
243
244         /**
245          * Configure the {@link Type} of file that should be loaded.
246          */

247         public Builder type(Type type) {
248             this.type = type;
249             return this;
250         }
251
252         public void setType(Type type) {
253             type(type);
254         }
255
256         @Override
257         public ProfileFile build() {
258             InputStream stream = content != null ? content :
259                                  FunctionalUtils.invokeSafely(() -> Files.newInputStream(contentLocation));
260
261             Validate.paramNotNull(type, "type");
262             Validate.paramNotNull(stream, "content");
263
264             try {
265                 return new ProfileFile(ProfileFileReader.parseFile(stream, type));
266             } finally {
267                 IoUtils.closeQuietly(stream, null);
268             }
269         }
270     }
271
272     /**
273      * A mechanism for merging multiple {@link ProfileFile}s together into a single file. This will merge their profiles and
274      * properties together.
275      */

276     public static final class Aggregator implements SdkBuilder<Aggregator, ProfileFile> {
277         private List<ProfileFile> files = new ArrayList<>();
278
279         /**
280          * Add a file to be aggregated. In the event that there is a duplicate profile/property pair in the files, files added
281          * earliest to this aggregator will take precedence, dropping the duplicated properties in the later files.
282          */

283         public Aggregator addFile(ProfileFile file) {
284             files.add(file);
285             return this;
286         }
287
288         @Override
289         public ProfileFile build() {
290             Map<String, Map<String, String>> aggregateRawProfiles = new LinkedHashMap<>();
291             for (int i = files.size() - 1; i >= 0; --i) {
292                 addToAggregate(aggregateRawProfiles, files.get(i));
293             }
294             return new ProfileFile(aggregateRawProfiles);
295         }
296
297         private void addToAggregate(Map<String, Map<String, String>> aggregateRawProfiles, ProfileFile file) {
298             Map<String, Profile> profiles = file.profiles();
299             for (Entry<String, Profile> profile : profiles.entrySet()) {
300                 aggregateRawProfiles.compute(profile.getKey(), (k, current) -> {
301                     if (current == null) {
302                         return new HashMap<>(profile.getValue().properties());
303                     } else {
304                         current.putAll(profile.getValue().properties());
305                         return current;
306                     }
307                 });
308             }
309         }
310     }
311 }
312