This repository was archived by the owner on Nov 22, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 213
Implement keywhiz.cli clone #1216
Draft
graysonchao
wants to merge
1
commit into
master
Choose a base branch
from
gchao/clone-secret
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
62 changes: 62 additions & 0 deletions
62
api/src/main/java/keywhiz/api/automation/v2/CloneSecretRequestV2.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
33
cli/src/main/java/keywhiz/cli/configs/CloneActionConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
158
cli/src/test/java/keywhiz/cli/commands/CloneActionTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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!")); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, good call