blob: bfba190af6d04cbdb73e33211bf6667f29a3d487 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.chef.filters;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertEqualsNoOrder;
import static org.testng.Assert.assertTrue;
import java.io.IOException;
import java.security.PrivateKey;
import javax.inject.Provider;
import javax.ws.rs.HttpMethod;
import org.jclouds.ContextBuilder;
import org.jclouds.chef.ChefApiMetadata;
import org.jclouds.crypto.Crypto;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.internal.SignatureWire;
import org.jclouds.logging.config.NullLoggingModule;
import org.jclouds.rest.internal.BaseRestApiTest.MockModule;
import org.jclouds.util.Strings2;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import com.google.common.base.Joiner;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.net.HttpHeaders;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
@Test(groups = { "unit" })
public class SignedHeaderAuthTest {
public static final String USER_ID = "spec-user";
public static final String BODY = "Spec Body";
// Base64.encode64(Digest::SHA1.digest("Spec Body")).chomp
public static final String HASHED_BODY = "DFteJZPVv6WKdQmMqZUQUumUyRs=";
public static final String TIMESTAMP_ISO8601 = "2009-01-01T12:00:00Z";
public static final String PATH = "/organizations/clownco";
// Base64.encode64(Digest::SHA1.digest("/organizations/clownco")).chomp
public static final String HASHED_CANONICAL_PATH = "YtBWDn1blGGuFIuKksdwXzHU9oE=";
public static final String REQUESTING_ACTOR_ID = "c0f8a68c52bffa1020222a56b23cccfa";
// Content hash is ???TODO
public static final String X_OPS_CONTENT_HASH = "DFteJZPVv6WKdQmMqZUQUumUyRs=";
public static final String[] X_OPS_AUTHORIZATION_LINES = new String[] {
"jVHrNniWzpbez/eGWjFnO6lINRIuKOg40ZTIQudcFe47Z9e/HvrszfVXlKG4",
"NMzYZgyooSvU85qkIUmKuCqgG2AIlvYa2Q/2ctrMhoaHhLOCWWoqYNMaEqPc",
"3tKHE+CfvP+WuPdWk4jv4wpIkAz6ZLxToxcGhXmZbXpk56YTmqgBW2cbbw4O",
"IWPZDHSiPcw//AYNgW1CCDptt+UFuaFYbtqZegcBd2n/jzcWODA7zL4KWEUy",
"9q4rlh/+1tBReg60QdsmDRsw/cdO1GZrKtuCwbuD4+nbRdVBKv72rqHX9cu0", "utju9jzczCyB+sSAQWrxSsXB/b8vV2qs0l4VD2ML+w==" };
// We expect Mixlib::Authentication::SignedHeaderAuth//sign to return this
// if passed the BODY above.
public static final Multimap<String, String> EXPECTED_SIGN_RESULT = ImmutableMultimap.<String, String> builder()
.put("X-Ops-Content-Hash", X_OPS_CONTENT_HASH).put("X-Ops-Userid", USER_ID).put("X-Ops-Sign", "version=1.0")
.put("X-Ops-Authorization-1", X_OPS_AUTHORIZATION_LINES[0])
.put("X-Ops-Authorization-2", X_OPS_AUTHORIZATION_LINES[1])
.put("X-Ops-Authorization-3", X_OPS_AUTHORIZATION_LINES[2])
.put("X-Ops-Authorization-4", X_OPS_AUTHORIZATION_LINES[3])
.put("X-Ops-Authorization-5", X_OPS_AUTHORIZATION_LINES[4])
.put("X-Ops-Authorization-6", X_OPS_AUTHORIZATION_LINES[5]).put("X-Ops-Timestamp", TIMESTAMP_ISO8601).build();
// Content hash for empty string
public static final String X_OPS_CONTENT_HASH_EMPTY = "2jmj7l5rSw0yVb/vlWAYkK/YBwk=";
public static final Multimap<String, String> EXPECTED_SIGN_RESULT_EMPTY = ImmutableMultimap
.<String, String> builder().put("X-Ops-Content-Hash", X_OPS_CONTENT_HASH_EMPTY).put("X-Ops-Userid", USER_ID)
.put("X-Ops-Sign", "version=1.0")
.put("X-Ops-Authorization-1", "N6U75kopDK64cEFqrB6vw+PnubnXr0w5LQeXnIGNGLRP2LvifwIeisk7QxEx")
.put("X-Ops-Authorization-2", "mtpQOWAw8HvnWErjzuk9AvUsqVmWpv14ficvkaD79qsPMvbje+aLcIrCGT1P")
.put("X-Ops-Authorization-3", "3d2uvf4w7iqwzrIscPnkxLR6o6pymR90gvJXDPzV7Le0jbfD8kmZ8AAK0sGG")
.put("X-Ops-Authorization-4", "09F1ftW80bLatJTA66Cw2wBz261r6x/abZhIKFJFDWLzyQGJ8ZNOkUrDDtgI")
.put("X-Ops-Authorization-5", "svLVXpOJKZZfKunsElpWjjsyNt3k8vpI1Y4ANO8Eg2bmeCPeEK+YriGm5fbC")
.put("X-Ops-Authorization-6", "DzWNPylHJqMeGKVYwGQKpg62QDfe5yXh3wZLiQcXow==")
.put("X-Ops-Timestamp", TIMESTAMP_ISO8601).build();
public static String PUBLIC_KEY;
public static String PRIVATE_KEY;
static {
try {
PUBLIC_KEY = Strings2.toStringAndClose(SignedHeaderAuthTest.class.getResourceAsStream("/pubkey.txt"));
PRIVATE_KEY = Strings2.toStringAndClose(SignedHeaderAuthTest.class.getResourceAsStream("/privkey.txt"));
} catch (IOException e) {
Throwables.propagate(e);
}
}
@Test
void canonicalizedPathRemovesMultipleSlashes() {
assertEquals(signing_obj.canonicalPath("///"), "/");
}
@Test
void canonicalizedPathRemovesTrailingSlash() {
assertEquals(signing_obj.canonicalPath("/path/"), "/path");
}
@Test
void shouldGenerateTheCorrectStringToSignAndSignature() {
HttpRequest request = HttpRequest.builder().method(HttpMethod.POST).endpoint("http://localhost/" + PATH)
.payload(BODY).build();
String expected_string_to_sign = new StringBuilder().append("Method:POST").append("\n").append("Hashed Path:")
.append(HASHED_CANONICAL_PATH).append("\n").append("X-Ops-Content-Hash:").append(HASHED_BODY).append("\n")
.append("X-Ops-Timestamp:").append(TIMESTAMP_ISO8601).append("\n").append("X-Ops-UserId:").append(USER_ID)
.toString();
assertEquals(signing_obj.createStringToSign("POST", HASHED_CANONICAL_PATH, HASHED_BODY, TIMESTAMP_ISO8601),
expected_string_to_sign);
assertEquals(signing_obj.sign(expected_string_to_sign), Joiner.on("").join(X_OPS_AUTHORIZATION_LINES));
request = signing_obj.filter(request);
Multimap<String, String> headersWithoutContentLength = LinkedHashMultimap.create(request.getHeaders());
headersWithoutContentLength.removeAll(HttpHeaders.CONTENT_LENGTH);
assertEqualsNoOrder(headersWithoutContentLength.values().toArray(), EXPECTED_SIGN_RESULT.values().toArray());
}
@Test
void shouldGenerateTheCorrectStringToSignAndSignatureWithNoBody() {
HttpRequest request = HttpRequest.builder().method(HttpMethod.DELETE).endpoint("http://localhost/" + PATH)
.build();
request = signing_obj.filter(request);
Multimap<String, String> headersWithoutContentLength = LinkedHashMultimap.create(request.getHeaders());
assertEqualsNoOrder(headersWithoutContentLength.entries().toArray(), EXPECTED_SIGN_RESULT_EMPTY.entries()
.toArray());
}
@Test
void shouldNotChokeWhenSigningARequestForAResourceWithALongName() {
StringBuilder path = new StringBuilder("nodes/");
for (int i = 0; i < 100; i++)
path.append('A');
HttpRequest request = HttpRequest.builder().method(HttpMethod.PUT)
.endpoint("http://localhost/" + path.toString()).payload(BODY).build();
signing_obj.filter(request);
}
@Test
void shouldReplacePercentage3FWithQuestionMarkAtUrl() {
StringBuilder path = new StringBuilder("nodes/");
path.append("test/cookbooks/myCookBook%3Fnum_versions=5");
HttpRequest request = HttpRequest.builder().method(HttpMethod.GET)
.endpoint("http://localhost/" + path.toString()).payload(BODY).build();
request = signing_obj.filter(request);
assertTrue(request.getRequestLine().contains("?num_versions=5"));
}
private SignedHeaderAuth signing_obj;
/**
* before class, as we need to ensure that the filter is threadsafe.
*
* @throws IOException
*
*/
@BeforeClass
protected void createFilter() throws IOException {
Injector injector = ContextBuilder.newBuilder(new ChefApiMetadata()).credentials(USER_ID, PRIVATE_KEY)
.modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule())).buildInjector();
HttpUtils utils = injector.getInstance(HttpUtils.class);
Crypto crypto = injector.getInstance(Crypto.class);
Supplier<PrivateKey> privateKey = injector.getInstance(Key.get(new TypeLiteral<Supplier<PrivateKey>>() {
}));
signing_obj = new SignedHeaderAuth(new SignatureWire(),
Suppliers.ofInstance(new Credentials(USER_ID, PRIVATE_KEY)), privateKey, new Provider<String>() {
@Override
public String get() {
return TIMESTAMP_ISO8601;
}
}, utils, crypto);
}
}