tokens icon indicating copy to clipboard operation
tokens copied to clipboard

Design improvements for FileSupplier.java

Open sina-golesorkhi opened this issue 8 years ago • 20 comments

Imagine that you have a class like following:

@Configuration
public class StupsOAuth2RestBean {

    private static final String UID_SCOPE = "uid";

    @Value("${tokens.accessTokenUri}")
    private String accessTokenURL;

    @Value("${tokens.configuration.tenant_service}")
    private String tokensConfigurationTemplateService;

    @Value("${tokens.configuration.tenant_service.scope}")
    private String tokensConfigurationTemplateServiceScope;

    @Bean
    public StupsOAuth2RestTemplate getInstance() {
        return new StupsOAuth2RestTemplate(new StupsTokensAccessTokenProvider(tokensConfigurationTemplateService,
                    getAccessTokens()));
    }

    @VisibleForTesting
    void setAccessTokenURL(final String accessTokenURL) {
        this.accessTokenURL = accessTokenURL;
    }

    @VisibleForTesting
    void setTokensConfigurationTemplateService(final String tokensConfigurationTemplateService) {
        this.tokensConfigurationTemplateService = tokensConfigurationTemplateService;
    }

    @VisibleForTesting
    void setTokensConfigurationTemplateServiceScope(final String tokensConfigurationTemplateServiceScope) {
        this.tokensConfigurationTemplateServiceScope = tokensConfigurationTemplateServiceScope;
    }

    private AccessTokens getAccessTokens() {
        try {
            //J-
            return Tokens.createAccessTokensWithUri(new URI(accessTokenURL))
                      .manageToken(tokensConfigurationTemplateService)
                      .addScope(tokensConfigurationTemplateServiceScope)
                      .addScope(UID_SCOPE).done().start();
            //J+
        } catch (final URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }
}

And the following unit test:

@RunWith(MockitoJUnitRunner.class)
public class StupsOAuth2RestBeanTest {

    @Mock
    private AccessTokenProvider accessTokenProvider;

    @Test
    public void getInstance_returnsNewInstanceOfStupsOAuth() throws Exception {
        final StupsOAuth2RestBean stupsBean = new StupsOAuth2RestBean();
        stupsBean.setAccessTokenURL("accessTokenURL");
        stupsBean.setTokensConfigurationTemplateService("tokensConfigurationTemplateService");
        stupsBean.setTokensConfigurationTemplateServiceScope("tokensConfigurationTemplateServiceScope");
        assertNotNull(stupsBean.getInstance());
    }
}

The test will pass but still you will get an exception as following.

org.zalando.stups.tokens.CredentialsUnavailableException: environment variable CREDENTIALS_DIR not set
.... 

Caused by: java.lang.IllegalStateException: environment variable CREDENTIALS_DIR not set
    at org.zalando.stups.tokens.FileSupplier.getCredentialsDir(FileSupplier.java:57)
    at org.zalando.stups.tokens.FileSupplier.get(FileSupplier.java:45)
    at org.zalando.stups.tokens.AbstractJsonFileBackedCredentialsProvider.getFile(AbstractJsonFileBackedCredentialsProvider.java:36)
    at org.zalando.stups.tokens.AbstractJsonFileBackedCredentialsProvider.read(AbstractJsonFileBackedCredentialsProvider.java:41)
    ... 33 common frames omitted

For the client code StupsOAuth2RestBean class there is no way of knowing about this missing configuration from outside and without looking into the source code I could not figure out that I have to configure environment variable because on line 51 and 55 of the FileSupplier.java this is read as env variable.

So my suggestions would be to document it so that it becomes more clear what this class is needing or the Java solution instead would be to read this value from outside (IoC) either by constructor or setters instead of static call to System class so that the client code knows about this and is forced to provide this in the compile time instead of reading from system environment (even this option can be provided if you want to read and write stuff in a static way using system environments).

  • the method org.zalando.stups.tokens.AccessTokenRefresher.run() is catching all throwables and swallowing them with only warn. That's why my test still passes but I get an stacktrace with WARN which doesn't seem to be reflect the expected behavior.

sina-golesorkhi avatar Apr 06 '16 14:04 sina-golesorkhi

@sina-golesorkhi thanks for reporting the problem, we will discuss this problem in the team.

First thing we will do, is to improve documentation.

jbellmann avatar Apr 15 '16 08:04 jbellmann

Hello everybody,

I am working a small library that can be used for testing purposes.

Basically I will create a @Rule that can be integrated in an JUnit Test for creating at test execution time all required dependencies.

Please have a little bit of more patience.

Best regards, Mihai

midumitrescu avatar Apr 15 '16 09:04 midumitrescu

@sina-golesorkhi I like the idea of providing configurable properties via constructor or setter, but to be backwards-compatible and easy-to-use, we should populate these attributes from the environment.

I would also suggest a fail-fast approach: A bad configuration should immediately throw a runtime exception.

harti2006 avatar Apr 18 '16 13:04 harti2006

@harti2006 I'm not quite getting why providing a configuration from outside and doing inversion of control would add complexity to the library. Actually by providing those options you are telling your client in compile time what you do need (specially with the constructor). If this was an optional configuration I could follow your argument but right now the library is throwing an exception which is logged but swallowed. To me if a code needs a configuration from me it should tell me in the compile that what is needed and reading stuff statically from a central place (e.g. System) is generally a bad idea because you are hiding it from the client thought that you want him provide you with that.

Regarding the fail-fast: As the client I would be happy to have that but such that this exception is bubbled up through the whole stack so that the client also gets the exception, though as I said in the previous paragraph by that time (runtime) the client wouldn't also be that much happy because the API didn't tell him (in compile time) that this exception is going to be thrown if one specific property is not configured/provided.

sina-golesorkhi avatar Apr 18 '16 14:04 sina-golesorkhi

@sina-golesorkhi

because of the code you used in your comment. If you are using Spring-Boot, are you aware of Spring-Boot support for 'tokens'?

It does not help to solve this concrete issue, but you don't have to write such custom-configurations as above and 'AccessTokens' will be available as injectable bean.

jbellmann avatar Apr 18 '16 14:04 jbellmann

@jbellmann I was not aware of this other project. Thank you for naming it I will have a look at it but as you also mentioned it is out of the scope of this issue because this issue is addressing a design improvement regarding this code base and one client might not want yet another dependency to another library;)

sina-golesorkhi avatar Apr 18 '16 14:04 sina-golesorkhi

@sina-golesorkhi @jbellmann @duergner @harti2006 @midumitrescu Hey, can we close this issue? It dates back to April 2016.

Or: We could add labels to suggest "enhancement" or "Help wanted" or both. Your call.

lasomethingsomething avatar Feb 07 '17 11:02 lasomethingsomething

Wouldn't close it - but Help wanted sounds definitely like a good idea.

duergner avatar Feb 07 '17 14:02 duergner

@jbellmann and me were thinking about fixing the issue with the mandatory environment variable, to prepare the 1.0.0 version of this library. @whiskeysierra what do you think?

harti2006 avatar Jun 08 '17 13:06 harti2006

What's the plan exactly? Nullable constructor parameter that fallsback to environment variable?

whiskeysierra avatar Jun 08 '17 14:06 whiskeysierra

I'm not sure. There are some cases (fixed test access tokens, or the test case above), where people don't want to worry about files and a credentials_dir at all. We need to provide an easy (and documented way) to turn the entire feature off.

harti2006 avatar Jun 08 '17 15:06 harti2006

I assume we are using semantic versioning, right? Since we are still at 0.x.y we can break the behaviour if we decide that it's worth the benefit. The next possibility to clean it up would be the next major version bump to 2.x which I'd guess is not on the immediate roadmap :stuck_out_tongue_winking_eye:.

whiskeysierra avatar Jun 08 '17 15:06 whiskeysierra

What about a constructor which does exactly this, i.e. turn off the feature? Should be the most obvious approach, right?

duergner avatar Jun 08 '17 17:06 duergner

I've created PR with one of the solutions. In short: check CREDENTIALS_DIR existence before checking files on it.

vadeg avatar Jun 11 '17 09:06 vadeg

But that does not solve the underlying "problem" in cases we don't want to rely on CREDENTIALS_DIR right?

duergner avatar Jun 11 '17 10:06 duergner

@duergner Yes. It solves the problem when you want to use AccessTokenRefresher but still see an error environment variable CREDENTIALS_DIR not set what is not correct.

If we don't want to rely on CREDENTIALS_DIR we can for example add a new method AccessTokensBuilder.withCredentialsDir(...). That will force to use FilesystemSecretRefresher instead of AccessTokenRefresher. There can be many cases.

vadeg avatar Jun 11 '17 15:06 vadeg

I just wanted to rephrase the issue that I saw a in my code as the client of this library that lead to create this ticket. There is a magic happening in this code which is the access to CREDENTIALS_DIR statically which is needed for this library to work but is hidden from the client point of view. This can be done by providing a method like @vadeg said (IoC) but I will even go further and make it mandatory and part of the constructor because without this value the code won't work so being part of the builder still makes it optional and doesn't signal that it has to be there.

sina-golesorkhi avatar Jun 11 '17 15:06 sina-golesorkhi

@sina-golesorkhi do you think the proposal regarding having two constructors where one has the explicit mentioning and the other falling back to the current behaviour would make sense? We would not break backward compatibility (we could mark the one without arguments as deprecated) but it should become more obvious.

duergner avatar Jun 11 '17 16:06 duergner

@duergner sure. That would be the best option :)

sina-golesorkhi avatar Jun 11 '17 19:06 sina-golesorkhi

@duergner I was thinking that CREDENTIALS_DIR is an optional feature. But now I see that library would not work without CREDENTIALS_DIR at all. For example if there is no CREDENTIALS_DIR set that means that AccessTokenRefresher (remote tokens fetch) will be used instead of FilesystemSecretRefresher (local tokens refresh). But JsonFileBackedClientCredentialsProvider and JsonFileBackedUserCredentialsProvider will be used by default for parsing client.json and user.json. Obviously, they will fail to parse these files because there is no CREDENTIALS_DIR.

vadeg avatar Jun 14 '17 07:06 vadeg