Skip to content
This repository has been archived by the owner on Nov 22, 2023. It is now read-only.

Commit

Permalink
expose API to createOrUpdate secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
alokmenghrajani committed Jun 15, 2016
1 parent 1a39820 commit 3ccefa2
Show file tree
Hide file tree
Showing 16 changed files with 506 additions and 52 deletions.
@@ -0,0 +1,85 @@
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 com.google.common.collect.ImmutableSet;
import keywhiz.api.validation.ValidBase64;

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

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

public static Builder builder() {
return new AutoValue_CreateOrUpdateSecretRequestV2.Builder()
.description("")
.metadata(ImmutableMap.of())
.expiry(0)
.type("");
}

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

public abstract Builder content(String content);
public abstract Builder description(String description);
public abstract Builder metadata(ImmutableMap<String, String> metadata);
public abstract Builder type(String type);
public abstract Builder expiry(long expiry);

/**
* @throws IllegalArgumentException if builder data is invalid.
*/
public CreateOrUpdateSecretRequestV2 build() {
// throws IllegalArgumentException if content not valid base64.
Base64.getDecoder().decode(content());

CreateOrUpdateSecretRequestV2 request = autoBuild();
return request;
}
}

/**
* Static factory method used by Jackson for deserialization
*/
@SuppressWarnings("unused")
@JsonCreator public static CreateOrUpdateSecretRequestV2 fromParts(
@JsonProperty("content") String content,
@JsonProperty("description") @Nullable String description,
@JsonProperty("metadata") @Nullable Map<String, String> metadata,
@JsonProperty("expiry") long expiry,
@JsonProperty("type") @Nullable String type) {
return builder()
.content(content)
.description(Strings.nullToEmpty(description))
.metadata(metadata == null ? ImmutableMap.of() : ImmutableMap.copyOf(metadata))
.expiry(expiry)
.type(Strings.nullToEmpty(type))
.build();
}

@JsonProperty("content") @ValidBase64 public abstract String content();
@JsonProperty("description") public abstract String description();
@JsonProperty("metadata") public abstract ImmutableMap<String, String> metadata();
@JsonProperty("expiry") public abstract long expiry();
@JsonProperty("type") public abstract String type();

@Override public final String toString() {
return MoreObjects.toStringHelper(this)
.add("content", "[REDACTED]")
.add("description", description())
.add("metadata", metadata())
.add("expiry", expiry())
.add("type", type())
.omitNullValues()
.toString();
}
}
11 changes: 3 additions & 8 deletions cli/src/main/java/keywhiz/cli/CliMain.java
Expand Up @@ -21,14 +21,8 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.Map;
import keywhiz.cli.configs.AddActionConfig;
import keywhiz.cli.configs.AssignActionConfig;
import keywhiz.cli.configs.CliConfiguration;
import keywhiz.cli.configs.DeleteActionConfig;
import keywhiz.cli.configs.DescribeActionConfig;
import keywhiz.cli.configs.ListActionConfig;
import keywhiz.cli.configs.LoginActionConfig;
import keywhiz.cli.configs.UnassignActionConfig;

import keywhiz.cli.configs.*;

/** Keywhiz ACL Command Line Management Utility */
public class CliMain {
Expand All @@ -41,6 +35,7 @@ public static void main(String[] args) throws Exception {
.put("list", new ListActionConfig())
.put("describe", new DescribeActionConfig())
.put("add", new AddActionConfig())
.put("update", new CreateOrUpdateActionConfig())
.put("delete", new DeleteActionConfig())
.put("assign", new AssignActionConfig())
.put("unassign", new UnassignActionConfig())
Expand Down
24 changes: 9 additions & 15 deletions cli/src/main/java/keywhiz/cli/CommandExecutor.java
Expand Up @@ -29,19 +29,9 @@
import java.util.Map;
import javax.annotation.Nullable;
import javax.inject.Inject;
import keywhiz.cli.commands.AddAction;
import keywhiz.cli.commands.AssignAction;
import keywhiz.cli.commands.DeleteAction;
import keywhiz.cli.commands.DescribeAction;
import keywhiz.cli.commands.ListAction;
import keywhiz.cli.commands.UnassignAction;
import keywhiz.cli.configs.AddActionConfig;
import keywhiz.cli.configs.AssignActionConfig;
import keywhiz.cli.configs.CliConfiguration;
import keywhiz.cli.configs.DeleteActionConfig;
import keywhiz.cli.configs.DescribeActionConfig;
import keywhiz.cli.configs.ListActionConfig;
import keywhiz.cli.configs.UnassignActionConfig;

import keywhiz.cli.commands.*;
import keywhiz.cli.configs.*;
import keywhiz.client.KeywhizClient;
import keywhiz.client.KeywhizClient.UnauthorizedException;
import okhttp3.HttpUrl;
Expand All @@ -52,9 +42,9 @@
import static java.lang.String.format;

public class CommandExecutor {
public static final String APP_VERSION = "2.0";
public static final String APP_VERSION = "2.1";

public enum Command { LOGIN, LIST, DESCRIBE, ADD, DELETE, ASSIGN, UNASSIGN }
public enum Command { LOGIN, LIST, DESCRIBE, ADD, UPDATE, DELETE, ASSIGN, UNASSIGN }

private final Path cookieDir = Paths.get(USER_HOME.value());

Expand Down Expand Up @@ -144,6 +134,10 @@ public void executeCommand() throws IOException {
new AddAction((AddActionConfig) commands.get(command), client, mapper).run();
break;

case UPDATE:
new CreateOrUpdateAction((CreateOrUpdateActionConfig) commands.get(command), client, mapper).run();
break;

case DELETE:
new DeleteAction((DeleteActionConfig) commands.get(command), client).run();
break;
Expand Down
128 changes: 128 additions & 0 deletions cli/src/main/java/keywhiz/cli/commands/CreateOrUpdateAction.java
@@ -0,0 +1,128 @@
/*
* 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.commands;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import keywhiz.cli.configs.CreateOrUpdateActionConfig;
import keywhiz.client.KeywhizClient;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;

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

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

private final CreateOrUpdateActionConfig createOrUpdateActionConfig;
private final KeywhizClient keywhizClient;
private final ObjectMapper mapper;

InputStream stream = System.in;

public CreateOrUpdateAction(CreateOrUpdateActionConfig createOrUpdateActionConfig, KeywhizClient client, ObjectMapper mapper) {
this.createOrUpdateActionConfig = createOrUpdateActionConfig;
this.keywhizClient = client;
this.mapper = mapper;
}

@Override public void run() {
String secretName = createOrUpdateActionConfig.secretName;

if (secretName == null || !validName(secretName)) {
throw new IllegalArgumentException(format("Invalid name, must match %s", VALID_NAME_PATTERN));
}

byte[] content = readSecretContent();
createOrUpdateSecret(secretName, content, getMetadata(), getExpiry());
}

private void createOrUpdateSecret(String secretName, byte[] content,
ImmutableMap<String, String> metadata, long expiry) {
try {
keywhizClient.createOrUpdateSecret(secretName, "", content, metadata, expiry);
logger.info("createOrUpdate secret '{}'.", secretName);
} catch (IOException e) {
throw Throwables.propagate(e);
}
}

private ImmutableMap<String, String> getMetadata() {
ImmutableMap<String, String> metadata = ImmutableMap.of();
String jsonBlob = createOrUpdateActionConfig.json;
if (jsonBlob != null && !jsonBlob.isEmpty()) {
TypeReference typeRef = new TypeReference<ImmutableMap<String, String>>() {};
try {
metadata = mapper.readValue(jsonBlob, typeRef);
} catch (IOException e) {
throw Throwables.propagate(e);
}
validateMetadata(metadata);
}
return metadata;
}

private long getExpiry() {
String expiry = createOrUpdateActionConfig.expiry;
if (expiry != null) {
try {
return Long.parseLong(expiry);
} catch (NumberFormatException e) {
}
DateTime dt = new DateTime(expiry);
return dt.getMillis();
}
return 0;
}

private byte[] readSecretContent() {
try {
byte[] content = ByteStreams.toByteArray(stream);
if (content.length == 0) {
throw new RuntimeException("Secret content empty!");
}
return content;
} catch (IOException e) {
throw Throwables.propagate(e);
}
}

private static void validateMetadata(ImmutableMap<String, String> metadata) {
for (ImmutableMap.Entry<String, String> entry : metadata.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();

// We want to perform strong validation of the metadata to make sure it is well formed.
if (!key.matches("(owner|group|mode)")) {
throw new IllegalArgumentException(format("Illegal metadata key %s", key));
}

if (key.equals("mode") && !value.matches("0[0-7]+")) {
throw new IllegalArgumentException(format("mode %s is not proper octal", value));
}
}
}
}
@@ -0,0 +1,34 @@
/*
* 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 = "Create or update secret.")
public class CreateOrUpdateActionConfig {
@Parameter(names = "--secret", description = "Name of the secret to add", required = true)
public String secretName;

@Parameter(names = "--json", description = "Metadata JSON blob")
public String json;

@Parameter(names = { "-e", "--expiry" }, description = "Secret expiry. For keystores, it is recommended to use the expiry of the earliest key. Format should be 2006-01-02T15:04:05Z or seconds since epoch.")
public String expiry;
}
17 changes: 17 additions & 0 deletions client/src/main/java/keywhiz/client/KeywhizClient.java
Expand Up @@ -30,6 +30,7 @@
import keywhiz.api.GroupDetailResponse;
import keywhiz.api.LoginRequest;
import keywhiz.api.SecretDetailResponse;
import keywhiz.api.automation.v2.CreateOrUpdateSecretRequestV2;
import keywhiz.api.model.Client;
import keywhiz.api.model.Group;
import keywhiz.api.model.SanitizedSecret;
Expand Down Expand Up @@ -155,6 +156,22 @@ public SecretDetailResponse createSecret(String name, String description, byte[]
return mapper.readValue(response, SecretDetailResponse.class);
}

public SecretDetailResponse createOrUpdateSecret(String name, String description, byte[] content,
ImmutableMap<String, String> metadata, long expiry) throws IOException {
checkArgument(!name.isEmpty());
checkArgument(content.length > 0, "Content must not be empty");

String b64Content = Base64.getEncoder().encodeToString(content);
CreateOrUpdateSecretRequestV2 request = CreateOrUpdateSecretRequestV2.builder()
.description(description)
.content(b64Content)
.metadata(metadata)
.expiry(expiry)
.build();
String response = httpPost(baseUrl.resolve(format("/admin/secrets/%s", name)), request);
return mapper.readValue(response, SecretDetailResponse.class);
}

public SecretDetailResponse secretDetailsForId(long secretId) throws IOException {
String response = httpGet(baseUrl.resolve(format("/admin/secrets/%d", secretId)));
return mapper.readValue(response, SecretDetailResponse.class);
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/java/keywhiz/KeywhizConfig.java
Expand Up @@ -179,7 +179,7 @@ public static class TemplatedDataSourceFactory extends DataSourceFactory {
}
}

// Sets the evaluated password before calling the parent's build method.
// Sets the evaluated password before calling the parent's create method.
@Override public ManagedDataSource build(MetricRegistry metricRegistry, String name) {
setPassword(getPassword());
return super.build(metricRegistry, name);
Expand Down
Expand Up @@ -145,10 +145,16 @@ public SecretBuilder withType(String type) {
*
* @return an instance of the newly created secret.
*/
public Secret build() {
public Secret create() {
secretDAO.createSecret(name, encryptedSecret, creator, metadata, expiry, description, type,
generationOptions);
return transformer.transform(secretDAO.getSecretByNameOne(name).get());
}

public Secret createOrUpdate() {
secretDAO.createOrUpdateSecret(name, encryptedSecret, creator, metadata, expiry, description, type,
generationOptions);
return transformer.transform(secretDAO.getSecretByNameOne(name).get());
}
}
}

0 comments on commit 3ccefa2

Please sign in to comment.