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