Skip to content
This repository was archived by the owner on Nov 22, 2023. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package keywhiz.api.automation.v2;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import keywhiz.api.validation.ValidBase64;

import javax.annotation.Nullable;
import java.util.Base64;
import java.util.Map;

@AutoValue public abstract class CloneSecretRequestV2 {
CloneSecretRequestV2() {} // prevent sub-classing

public static Builder builder() {
return new AutoValue_CloneSecretRequestV2.Builder()
.name("")
.newName("");
}

@AutoValue.Builder public abstract static class Builder {
// intended to be package-private
abstract CloneSecretRequestV2 autoBuild();

public abstract Builder name(String name);
public abstract Builder newName(String newName);

/**
* @throws IllegalArgumentException if builder data is invalid.
*/
public CloneSecretRequestV2 build() {
// throws IllegalArgumentException if content not valid base64.
return autoBuild();
}
Comment on lines +34 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this comment still applicable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, good call

}

/**
* Static factory method used by Jackson for deserialization
*/
@SuppressWarnings("unused")
@JsonCreator public static CloneSecretRequestV2 fromParts(
@JsonProperty("name") String name,
@JsonProperty("newName") String newName) {
return builder()
.name(name)
.newName(newName)
.build();
}
Comment on lines +40 to +51
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess while we're on the subject of comments, since @JsonCreator literally means "static factory method for Jackson", we could probably ditch the comment.


@JsonProperty("name") public abstract String name();
@JsonProperty("newName") public abstract String newName();

@Override public final String toString() {
return MoreObjects.toStringHelper(this)
.add("name", name())
.add("newName", newName())
.toString();
}
}
6 changes: 6 additions & 0 deletions cli/src/main/java/keywhiz/cli/CommandExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import javax.inject.Inject;
import keywhiz.cli.commands.AddAction;
import keywhiz.cli.commands.AssignAction;
import keywhiz.cli.commands.CloneAction;
import keywhiz.cli.commands.DeleteAction;
import keywhiz.cli.commands.DescribeAction;
import keywhiz.cli.commands.ListAction;
Expand All @@ -43,6 +44,7 @@
import keywhiz.cli.configs.AddActionConfig;
import keywhiz.cli.configs.AssignActionConfig;
import keywhiz.cli.configs.CliConfiguration;
import keywhiz.cli.configs.CloneActionConfig;
import keywhiz.cli.configs.DeleteActionConfig;
import keywhiz.cli.configs.DescribeActionConfig;
import keywhiz.cli.configs.ListActionConfig;
Expand All @@ -67,6 +69,7 @@ public class CommandExecutor {
public enum Command {
ADD,
ASSIGN,
CLONE,
DELETE,
DESCRIBE,
LIST,
Expand Down Expand Up @@ -203,6 +206,9 @@ public void executeCommand() throws IOException {
new RenameAction((RenameActionConfig) commands.get(command), client).run();
break;

case CLONE:
new CloneAction((CloneActionConfig) commands.get(command), client, printing).run();

case LOGIN:
// User is already logged in at this point
break;
Expand Down
69 changes: 69 additions & 0 deletions cli/src/main/java/keywhiz/cli/commands/CloneAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed 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 keywhiz.cli.commands;

import keywhiz.api.SecretDetailResponse;
import keywhiz.api.model.Group;
import keywhiz.cli.Printing;
import keywhiz.cli.configs.CloneActionConfig;
import keywhiz.client.KeywhizClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;

import static java.lang.String.format;
import static keywhiz.cli.Utilities.VALID_NAME_PATTERN;
import static keywhiz.cli.Utilities.validName;

public class CloneAction implements Runnable {
private static final Logger logger = LoggerFactory.getLogger(CloneAction.class);

private final CloneActionConfig cloneActionConfig;
private final KeywhizClient keywhizClient;
private final Printing printing;

public CloneAction(CloneActionConfig cloneActionConfig, KeywhizClient client, Printing printing) {
this.cloneActionConfig = cloneActionConfig;
this.keywhizClient = client;
this.printing = printing;
}

@Override public void run() {
if (cloneActionConfig.newName == null || !validName(cloneActionConfig.newName)) {
throw new IllegalArgumentException(format("Invalid name, must match %s", VALID_NAME_PATTERN));
}

try {
logger.info("Cloning secret '{}' to new name '{}'", cloneActionConfig.name, cloneActionConfig.newName);
long existingId = keywhizClient.getSanitizedSecretByName(cloneActionConfig.name).id();
SecretDetailResponse existingSecretDetails = keywhizClient.secretDetailsForId(existingId);

long newId = keywhizClient.cloneSecret(cloneActionConfig.name, cloneActionConfig.newName).id;
for (Group group : existingSecretDetails.groups) {
keywhizClient.grantSecretToGroupByIds(newId, group.getId());
}
printing.printSecretWithDetails(newId);
} catch (KeywhizClient.NotFoundException e) {
throw new AssertionError("Source secret doesn't exist.");
} catch (KeywhizClient.ConflictException e) {
throw new AssertionError("New secret name is already in use.");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
33 changes: 33 additions & 0 deletions cli/src/main/java/keywhiz/cli/configs/CloneActionConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed 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 keywhiz.cli.configs;

import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;

import java.util.List;

@Parameters(commandDescription = "Clone an existing secret")
public class CloneActionConfig {

@Parameter(names = "--name", description = "Name of the secret to clone", required = true)
public String name;

@Parameter(names = "--new-name", description = "Name for the clone",
required = true)
public String newName;
}
158 changes: 158 additions & 0 deletions cli/src/test/java/keywhiz/cli/commands/CloneActionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright (C) 2023 Square, Inc.
*
* Licensed 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 keywhiz.cli.commands;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import keywhiz.api.ApiDate;
import keywhiz.api.SecretDetailResponse;
import keywhiz.api.model.Group;
import keywhiz.api.model.SanitizedSecret;
import keywhiz.api.model.Secret;
import keywhiz.cli.Printing;
import keywhiz.cli.configs.CloneActionConfig;
import keywhiz.client.KeywhizClient;
import keywhiz.client.KeywhizClient.NotFoundException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import java.io.IOException;

import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class CloneActionTest {
private static final ApiDate NOW = ApiDate.now();

@Rule public MockitoRule mockito = MockitoJUnit.rule();

@Mock KeywhizClient keywhizClient;
@Mock Printing printing;

CloneActionConfig cloneActionConfig;
CloneAction cloneAction;

Secret oldSecret = new Secret(0, "oldSecret", null, null, () -> "c2VjcmV0MQ==", "checksum", NOW, null, NOW, null, null, null,
ImmutableMap.of(), 0, 1L, NOW, null);
Secret newSecret = new Secret(1, "newSecret", null, null, () -> "c2VjcmV0MQ==", "checksum", NOW, null, NOW, null, null, null,
ImmutableMap.of(), 0, 1L, NOW, null);
Group group = new Group(5, "group", null, null, null, null, null, null);

SecretDetailResponse detailResponse = SecretDetailResponse.fromSecret(
newSecret, ImmutableList.of(group), ImmutableList.of());

@Before
public void setUp() throws IOException {
cloneActionConfig = new CloneActionConfig();
cloneAction = new CloneAction(cloneActionConfig, keywhizClient, printing);

when(keywhizClient.getSanitizedSecretByName("oldSecret")).thenReturn(SanitizedSecret.fromSecret(oldSecret));
when(keywhizClient.secretDetailsForId(0)).thenReturn(detailResponse);
}

@Test
public void cloneCopiesSecret() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenReturn(detailResponse);

cloneAction.run();
verify(keywhizClient).cloneSecret("oldSecret", "newSecret");
}

@Test
public void cloneCopiesGroupAssignments() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenReturn(detailResponse);

cloneAction.run();
verify(keywhizClient).grantSecretToGroupByIds(1, 5);
}

@Test
public void cloneCallsPrint() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenReturn(detailResponse);

cloneAction.run();
verify(printing).printSecretWithDetails(1);
}

@Test
public void cloneThrowsIfOldSecretDoesNotExist() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenThrow(NotFoundException.class);
AssertionError ex = assertThrows(
AssertionError.class,
() -> cloneAction.run()
);
assertTrue(ex.getMessage().contains("Source secret doesn't exist"));
}

@Test
public void cloneThrowsIfNewNameIsInvalid() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "totally invalid name!!!😱";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenReturn(detailResponse);

IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> cloneAction.run()
);
assertTrue(ex.getMessage().contains("Invalid name"));
}

@Test
public void cloneThrowsIfNewNameConflicts() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenThrow(KeywhizClient.ConflictException.class);
AssertionError ex = assertThrows(
AssertionError.class,
() -> cloneAction.run()
);
assertTrue(ex.getMessage().contains("New secret name is already in use"));
}

@Test
public void cloneWrapsIOException() throws Exception {
cloneActionConfig.name = "oldSecret";
cloneActionConfig.newName = "newSecret";

when(keywhizClient.cloneSecret("oldSecret", "newSecret")).thenThrow(new IOException("uh oh spaghettios!"));
RuntimeException ex = assertThrows(
RuntimeException.class,
() -> cloneAction.run()
);
assertTrue(ex.getMessage().contains("uh oh spaghettios!"));
}
}
6 changes: 6 additions & 0 deletions client/src/main/java/keywhiz/client/KeywhizClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import keywhiz.api.GroupDetailResponse;
import keywhiz.api.LoginRequest;
import keywhiz.api.SecretDetailResponse;
import keywhiz.api.automation.v2.CloneSecretRequestV2;
import keywhiz.api.automation.v2.CreateClientRequestV2;
import keywhiz.api.automation.v2.CreateGroupRequestV2;
import keywhiz.api.automation.v2.CreateSecretRequestV2;
Expand Down Expand Up @@ -384,6 +385,11 @@ public List<SecretSeries> getDeletedSecretsByName(String name) throws IOExceptio
});
}

public SecretDetailResponse cloneSecret(String sourceName, String newName) throws IOException {
String response = httpPost(baseUrl.resolve("clone"), CloneSecretRequestV2.fromParts(sourceName, newName));
return mapper.readValue(response, SecretDetailResponse.class);
}

public boolean isLoggedIn() throws IOException {
HttpUrl url = baseUrl.resolve("/admin/me");
Call call = client.newCall(new Request.Builder().get().url(url).build());
Expand Down
Loading