diff --git a/src/main/java/com/amihaiemil/docker/Auth.java b/src/main/java/com/amihaiemil/docker/Auth.java new file mode 100644 index 00000000..db24150a --- /dev/null +++ b/src/main/java/com/amihaiemil/docker/Auth.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2018, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker; + +/** + * Authentication for Docker API. + * @author George Aristy (george.aristy@gmail.com) + * @version $Id$ + * @see Authentication + * @since 0.0.1 + * @todo #99:30min Implement a new auth named 'Token' that will hold the user's + * identity token from the auth endpoint. Implement some operation that would + * call the /auth endpoint and obtain a token. + */ +public interface Auth { + /** + * This {@link Auth} as a Base-64 encoded string. + * @return This auth as a base64-encoded string. + */ + String encoded(); +} diff --git a/src/main/java/com/amihaiemil/docker/AuthHttpClient.java b/src/main/java/com/amihaiemil/docker/AuthHttpClient.java new file mode 100644 index 00000000..299c6b56 --- /dev/null +++ b/src/main/java/com/amihaiemil/docker/AuthHttpClient.java @@ -0,0 +1,144 @@ +/** + * Copyright (c) 2018, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker; + +import java.io.IOException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; + +/** + * An authenticated HttpClient. + * @author George Aristy (george.aristy@gmail.com) + * @version $Id$ + * @since 0.0.1 + * @todo #99:30min Start decorating our internal HttpClients with this one + * for calls that require authentication headers. + */ +final class AuthHttpClient implements HttpClient { + /** + * The decorated HttpClient. + */ + private final HttpClient origin; + /** + * The authentication to inject into requests. + */ + private final Auth authentication; + + /** + * Ctor. + * @param origin The HttpClient to decorate. + * @param authentication The authentication to inject into requests. + */ + AuthHttpClient(final HttpClient origin, final Auth authentication) { + this.origin = origin; + this.authentication = authentication; + } + + @Override + public HttpParams getParams() { + throw new UnsupportedOperationException(); + } + + @Override + public ClientConnectionManager getConnectionManager() { + throw new UnsupportedOperationException(); + } + + @Override + public HttpResponse execute(final HttpUriRequest request) + throws IOException, ClientProtocolException { + final String header = "X-Registry-Auth"; + if (!request.containsHeader(header)) { + request.setHeader(header, this.authentication.encoded()); + } + return this.origin.execute(request); + } + + @Override + public HttpResponse execute( + final HttpUriRequest request, final HttpContext context + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + @Override + public HttpResponse execute( + final HttpHost target, final HttpRequest request + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + @Override + public HttpResponse execute( + final HttpHost target, final HttpRequest request, + final HttpContext context + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + @Override + public T execute( + final HttpUriRequest request, + final ResponseHandler responseHandler + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + @Override + public T execute( + final HttpUriRequest request, + final ResponseHandler responseHandler, + final HttpContext context + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + @Override + public T execute( + final HttpHost target, final HttpRequest request, + final ResponseHandler responseHandler + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } + + // @checkstyle ParameterNumber (5 lines) + @Override + public T execute( + final HttpHost target, final HttpRequest request, + final ResponseHandler responseHandler, + final HttpContext context + ) throws IOException, ClientProtocolException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/amihaiemil/docker/Credentials.java b/src/main/java/com/amihaiemil/docker/Credentials.java new file mode 100644 index 00000000..37f8cb69 --- /dev/null +++ b/src/main/java/com/amihaiemil/docker/Credentials.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.function.Supplier; +import javax.json.Json; + +/** + * An {@link Auth} supporting bare user credentials. + * @author George Aristy (george.aristy@gmail.com) + * @version $Id$ + * @see Authentication + * @since 0.0.1 + */ +public final class Credentials implements Auth { + /** + * The base64-encoded JSON structure holding the credentials. + */ + private final Supplier encoded; + + /** + * Ctor. + * @param user The username. + * @param pwd The user's password. + * @param email The user's email address. + * @param server Domain/IP without a protocol. + * @checkstyle ParameterNumber (4 lines) + */ + public Credentials( + final String user, final String pwd, + final String email, final String server + ) { + this.encoded = () -> Base64.getEncoder().encodeToString( + Json.createObjectBuilder() + .add("username", user) + .add("password", pwd) + .add("email", email) + .add("serveraddress", server) + .build().toString() + .getBytes(StandardCharsets.UTF_8) + ); + } + + @Override + public String encoded() { + return this.encoded.get(); + } +} diff --git a/src/main/java/com/amihaiemil/docker/RtImages.java b/src/main/java/com/amihaiemil/docker/RtImages.java index 7aeefe50..41144dda 100644 --- a/src/main/java/com/amihaiemil/docker/RtImages.java +++ b/src/main/java/com/amihaiemil/docker/RtImages.java @@ -93,11 +93,6 @@ public Iterable iterate() throws IOException { } } - // @todo #83:30min Several API calls required an authentication header as - // explained here: - // https://docs.docker.com/engine/api/v1.35/#section/Authentication - // (including Images.create()). Find a way to make a reusable object from - // that action and introduce it here. // @checkstyle ParameterNumber (4 lines) @Override public Images create( diff --git a/src/test/java/com/amihaiemil/docker/AuthHttpClientTestCase.java b/src/test/java/com/amihaiemil/docker/AuthHttpClientTestCase.java new file mode 100644 index 00000000..c4fbd2f2 --- /dev/null +++ b/src/test/java/com/amihaiemil/docker/AuthHttpClientTestCase.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2018, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker; + +import org.apache.http.Header; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Unit tests for {@link AuthHttpClient}. + * @author George Aristy (george.aristy@gmail.com) + * @version $Id$ + * @since 0.0.1 + */ +public final class AuthHttpClientTestCase { + /** + * Mock HttpClient that does nothing. + */ + private static HttpClient noOpClient; + + /** + * Setup the mock http client. + * @throws Exception If something does wrong. + */ + @BeforeClass + public static void setup() throws Exception { + noOpClient = Mockito.mock(HttpClient.class); + Mockito.when(noOpClient.execute(Mockito.any(HttpUriRequest.class))) + .thenReturn(null); + } + + /** + * Must inject the X-Registry-Auth header if absent and set it to the + * auth's value. + * @throws Exception If something goes wrong. + */ + @Test + public void injectsHeaderIfAbsent() throws Exception { + final HttpUriRequest request = new HttpGet(); + new AuthHttpClient(noOpClient, () -> "123").execute(request); + MatcherAssert.assertThat( + request.getFirstHeader("X-Registry-Auth").getValue(), + Matchers.is("123") + ); + } + + /** + * Leaves the request's header instact if it exists. + * @throws Exception If something goes wrong. + */ + @Test + public void leavesExistingHeaderAlone() throws Exception { + final Header auth = new BasicHeader("X-Registry-Auth", "12356"); + final HttpUriRequest request = new HttpGet(); + request.setHeader(auth); + new AuthHttpClient(noOpClient, () -> "abc").execute(request); + MatcherAssert.assertThat( + request.getFirstHeader("X-Registry-Auth"), + Matchers.is(auth) + ); + } +} diff --git a/src/test/java/com/amihaiemil/docker/CredentialsTestCase.java b/src/test/java/com/amihaiemil/docker/CredentialsTestCase.java new file mode 100644 index 00000000..ee919a8a --- /dev/null +++ b/src/test/java/com/amihaiemil/docker/CredentialsTestCase.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2018, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import javax.json.Json; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Unit tests for {@link Credentials}. + * @author George Aristy (george.aristy@gmail.com) + * @version $Id$ + * @since 0.0.1 + */ +public final class CredentialsTestCase { + /** + * Correctly encodes to base64 all attributes as a JSON object. + */ + @Test + public void correctEncoding() { + MatcherAssert.assertThat( + new Credentials( + "user", "pass", "john@doe.com", "server" + ).encoded(), + Matchers.is( + Base64.getEncoder().encodeToString( + Json.createObjectBuilder() + .add("username", "user") + .add("password", "pass") + .add("email", "john@doe.com") + .add("serveraddress", "server") + .build().toString() + .getBytes(StandardCharsets.UTF_8) + ) + ) + ); + } +}