Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Correctly handle nested configs #183

Merged
merged 3 commits into from
Aug 21, 2018

Conversation

AlainODea
Copy link
Contributor

Problem Statement

Issue #141 states:

Existing ~/.aws/config with nested properties are overwritten improperly by this tool.
For example, s3 configuration uses indentation for its properties e.g.

[profile development]
aws_access_key_id=foo
aws_secret_access_key=bar
s3 =
  max_concurrent_requests = 20
  max_queue_size = 10000

When awscli is run, the config is written out like so:

[profile development]
aws_access_key_id=foo
aws_secret_access_key=bar
s3 =
max_concurrent_requests = 20
max_queue_size = 10000

which is not correctly read by aws s3 commands!

Specifically, the following test https://github.com/oktadeveloper/okta-aws-cli-assume-role/blob/054c45c5b0b58195db45bd25b528c0436331d7ed/src/test/java/com/okta/tools/aws/settings/ConfigurationTest.java#L110-L130 if modified to contain s3 keys as follows:

    @Test
    void addOrUpdateProfileToExistingProfile() throws IOException {
        final String existingCombined = manualRole + "\n"
                + "s3 =\n"
                + "    max_queue_size = 1000" + "\n\n" + existingProfile;

        final StringReader configurationReader = new StringReader(existingCombined);
        final StringWriter configurationWriter = new StringWriter();
        final Configuration configuration = new Configuration(configurationReader);

        final String updatedPrefix = "updated_";
        final String expected = "[profile " + profileName + "]\n"
                + Configuration.ROLE_ARN + " = " + updatedPrefix + role_arn + "\n"
                + SOURCE_PROFILE + " = " + profileName + "_source\n"
                + Configuration.REGION + " = " + region + "\n"
                + "s3 =\n"
                + "    max_queue_size = 1000"
                + "\n\n" + existingProfile;


        configuration.addOrUpdateProfile(profileName, updatedPrefix + role_arn);
        configuration.save(configurationWriter);

        String given = StringUtils.remove(configurationWriter.toString().trim(), '\r');

        assertEquals(expected, given);
    }

will not pass.

Solution

  • Add Apache Commons Configuration2

  • Adapt configuration2's INIConfiguration to support AWS quirks

  • Remove Ini4j (unmaintained, has park page for website)

  • Refactor file handling code

Resolves #141

Apologies for the extensiveness of this change. The configuration
handling likely needs a refactor to make it easier to support future
modification.

Apologies for the extensiveness of this change. The configuration
handling likely needs a refactor to make it easier to support future
modification.

 - Add Apache Commons Configuration2

 - Adapt configuration2's INIConfiguration to support AWS quirks

 - Remove Ini4j (unmaintained, has park page for website)

 - Refactor file handling code

Resolves oktadev#141
Copy link
Contributor Author

@AlainODea AlainODea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some context on the over 1,800 lines of changes:

  • 875 is directly copied with minimal modification from Apache Commons Configuration 2
  • 294 lines are new tests


/**
* A simple abstract class used to load AWS CLI settings files like the `credentials` or `config` files.
*/
public abstract class Settings {

final Ini settings = new Ini();
private final AwsINIConfiguration settings;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A huge chunk of the change stems from hiding this as exposing it causing Ini4j's API to leak into everywhere and my original attempt at this had Apache Commons Configuration2's API leaked into everywhere.

@@ -0,0 +1,875 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 before calling me on this. I admit that this is a copy/paste from Apache Commons Configuration2. I tried to extend their class and it wasn't open to extension in this manner. I will look into contributing an extension point to their project so that we can later replace this with something much cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
import org.apache.commons.configuration2.tree.*;
import org.apache.commons.lang3.StringUtils;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

➕ added this so I could effectively right trim.

String line = in.readLine();
while (line != null)
{
line = StringUtils.stripEnd(line, null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
key = line;
}
key = StringUtils.stripEnd(key, null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void instantiateInvalidConfiguration() {
assertThrows(IOException.class, () -> new Configuration(new StringReader("someinvalidini")));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is technically a valid INI file. I couldn't figure out how to construct an invalid INI file. Ini4j got this wrong.

Reader reader = FileHelper.getReader(FileHelper.getOktaDirectory(), "profiles");

return new MultipleProfile(reader);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 I reordered the methods here to be in step-down order (Clean Code). Sorry for the diff carnage.

FileHelper.usingPath(FileHelper.getOktaDirectory().resolve("profiles"), (reader, writer) -> {
MultipleProfile multipleProfile = new MultipleProfile(reader);
multipleProfile.addOrUpdateProfile(environment.oktaProfile, environment.awsRoleToAssume, start);
multipleProfile.save(writer);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 I replaced the explicit resource management with a utility method that structurally ensures correct use without burdening every site of use.

private Path getSessionPath() throws IOException {
return FileHelper.resolveFilePath(FileHelper.getOktaDirectory(), ".current-session");
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 a good chunk of the diff in this file is method reordering to show methods in the order they are used (step down rule from Clean Code).

}

public interface PathW {
void useFile(Writer writer) throws IOException;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I'm not sure I love the names of these utility methods or interfaces, but I couldn't come up with anything better. I like the outcome in code tho.

@@ -60,21 +59,8 @@ public Credentials(Reader reader) throws IOException {
*/
public void addOrUpdateProfile(String name, String awsAccessKey, String awsSecretKey, String awsSessionToken) {
name = "default".equals(name) ? "default" : name + environment.credentialsSuffix;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also be using DEFAULT_PROFILE_NAME instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. I’ll fix that. Good catch.

 - Use defined constant instead of magic string
@AlainODea
Copy link
Contributor Author

@frosforever thank you for the code review. Any other concerns before I merge this?

Copy link
Contributor

@frosforever frosforever left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Instant expiry = Instant.parse(profileExpiry);
return expiry;
private Instant getExpiry(String profile) {
String profileExpiry = getProperty(profile, "profile_expiry");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use static PROFILE_EXPIRY

public MultipleProfile(Reader reader) throws IOException {
super(reader);
private String getRoleArn(String profile) {
return getProperty(profile, "okta_roleArn");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this use static OKTA_ROLE_ARN

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again. I've fixed these.

 - Use defined constants instead of magic strings
@AlainODea AlainODea merged commit 084209d into oktadev:master Aug 21, 2018
@AlainODea AlainODea deleted the ao-BUG-breaks-nested-config branch September 29, 2018 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Configurations with nested configs improperly handeled
2 participants