Skip to content
Open
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
40 changes: 40 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ node {
}
```

### Username with Password (JSON format)

A *username* and *password* pair.

- Value: Valid JSON describing an object with a `username` field and and `password` field.
- Tags:
- `jenkins:credentials:type` = `jsonUsernamePassword`

#### Example

AWS CLI:

```bash
aws secretsmanager create-secret --name 'artifactory' --secret-string '{ username: "joe", password: "supersecret" }' --tags 'Key=jenkins:credentials:type,Value=jsonUsernamePassword' --description 'Acme Corp Artifactory login'
```

Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format Username and Password.

### SSH User Private Key

An SSH *private key*, with a *username*.
Expand Down Expand Up @@ -218,6 +236,28 @@ node {
}
```

### SSH User Private Key (JSON format)

An SSH *private key*, with a *username* and optional *passphrase* for the private key.

- Value: Valid JSON describing an object with a `username` field, `privatekey` field and optionally a `passphrase` field.
- Tags:
- `jenkins:credentials:type` = `jsonSshUserPrivateKey`

**Note:** Unlike the non-JSON-format version, the passphrase field is supported. This is because the passphrase is contained within the AWS secret data, not as a (non-secret) tag.

#### Example

AWS CLI:

```bash
ssh-keygen -t rsa -b 4096 -C '[email protected]' -f id_rsa -N mySecretPassPhrase
jq -n --arg key "$(cat id_rsa)" '{ username: "joe", privatekey: $key, passphrase: "mySecretPassPhrase" }' >json
aws secretsmanager create-secret --name 'ssh-key' --secret-string 'file://json' --tags 'Key=jenkins:credentials:type,Value=jsonSshUserPrivateKey' --description 'Acme Corp SSH key'
```

Declarative and Scripted Pipeline behavior is (exactly) the same as non-JSON-format SSH User Private Key.

### Certificate

A client certificate *keystore* in PKCS#12 format, encrypted with a zero-length password.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package io.jenkins.plugins.credentials.secretsmanager.factory;

import java.util.function.Supplier;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import com.cloudbees.plugins.credentials.CredentialsUnavailableException;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.util.Secret;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import net.sf.json.JSON;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;

/**
* Base class for credentials where the AWS secret data is JSON format instead
* of raw data.
*/
@Restricted(NoExternalUse.class)
public abstract class BaseAwsJsonCredentials extends BaseStandardCredentials {
/** How to access the JSON */
private final Supplier<Secret> json;

/**
* Constructs a new instance with new data.
*
* @param id The value for {@link #getId()}.
* @param description The value for {@link #getDescription()}.
* @param json Supplies the data that {@link #getSecretJson()} will
* decode.
*/
protected BaseAwsJsonCredentials(String id, String description, Supplier<Secret> json) {
super(id, description);
this.json = json;
}

/**
* Constructs an instance that is an unchanging snapshot of another instance.
*
* @param toSnapshot The instance to be copied.
*/
protected BaseAwsJsonCredentials(BaseAwsJsonCredentials toSnapshot) {
super(toSnapshot.getId(), toSnapshot.getDescription());
final Secret secretDataToSnapshot = toSnapshot.json.get();
this.json = new Snapshot<Secret>(secretDataToSnapshot);
}

// Note:
// We MUST NOT tell anyone what the JSON is, or give any hints as to its
// contents, as that could then leak sensitive data so, if anything goes wrong,
// we have to suppress the informative exception(s) and just tell the user that
// it didn't work.

/**
* Reads the secret JSON and returns the field requested.
*
* @param secretJson The {@link JSONObject} we're going to look in, which likely
* came from {@link #getSecretJson()}.
* @param fieldname The (top-level) field that we want (which must be a
* {@link String}).
* @return The contents of that JSON field.
* @throws CredentialsUnavailableException if the JSON is missing the field, or
* the field is not a {@link String}.
*/
protected String getMandatoryField(@NonNull JSONObject secretJson, @NonNull String fieldname) {
final String fieldValue;
try {
fieldValue = secretJson.getString(fieldname);
} catch (JSONException | NullPointerException ex) {
throw new CredentialsUnavailableException("secret", Messages.wrongJsonError(getId(), fieldname));
}
return fieldValue;
}

/**
* Reads the secret JSON and returns the field requested.
*
* @param secretJson The {@link JSONObject} we're going to look in, which likely
* came from {@link #getSecretJson()}.
* @param fieldname The (top-level) field that we want (which must be a
* {@link String}).
* @return The contents of that JSON field.
*/
protected String getOptionalField(@NonNull JSONObject secretJson, @NonNull String fieldname) {
final String fieldValue = secretJson.optString(fieldname);
return fieldValue;
}

/**
* Reads the secret JSON and returns it.
*
* @return The contents of that JSON field.
* @throws CredentialsUnavailableException if there is no JSON, or it is not
* valid JSON.
*/
@NonNull
protected JSONObject getSecretJson() {
final Secret secret = json.get();
final String rawSecretJson = secret == null ? "" : secret.getPlainText();
if (rawSecretJson.isEmpty()) {
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
}
final JSON parsedJson;
try {
parsedJson = JSONSerializer.toJSON(rawSecretJson);
} catch (JSONException ex) {
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
}
// if we got this far then we have some syntactically-valid JSON
// ... but it might not be a JSON object containing the field we wanted.
final JSONObject jsonObject;
try {
jsonObject = (JSONObject) parsedJson;
} catch (ClassCastException ex) {
throw new CredentialsUnavailableException("secret", Messages.noValidJsonError(getId()));
}
return jsonObject;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import io.jenkins.plugins.credentials.secretsmanager.factory.certificate.AwsCertificateCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.file.AwsFileCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsJsonSshUserPrivateKey;
import io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key.AwsSshUserPrivateKey;
import io.jenkins.plugins.credentials.secretsmanager.factory.string.AwsStringCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsJsonUsernamePasswordCredentials;
import io.jenkins.plugins.credentials.secretsmanager.factory.username_password.AwsUsernamePasswordCredentials;

import java.util.Map;
Expand Down Expand Up @@ -46,8 +48,12 @@ public static Optional<StandardCredentials> create(String arn, String name, Stri
return Optional.of(new AwsStringCredentials(name, description, new SecretSupplier(client, arn)));
case Type.usernamePassword:
return Optional.of(new AwsUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn), username));
case Type.jsonUsernamePassword:
return Optional.of(new AwsJsonUsernamePasswordCredentials(name, description, new SecretSupplier(client, arn)));
case Type.sshUserPrivateKey:
return Optional.of(new AwsSshUserPrivateKey(name, description, new StringSupplier(client, arn), username));
case Type.jsonSshUserPrivateKey:
return Optional.of(new AwsJsonSshUserPrivateKey(name, description, new SecretSupplier(client, arn)));
case Type.certificate:
return Optional.of(new AwsCertificateCredentials(name, description, new SecretBytesSupplier(client, arn)));
case Type.file:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ public abstract class Type {
public static final String certificate = "certificate";
public static final String file = "file";
public static final String usernamePassword = "usernamePassword";
public static final String jsonUsernamePassword = "jsonUsernamePassword";
public static final String sshUserPrivateKey = "sshUserPrivateKey";
public static final String jsonSshUserPrivateKey = "jsonSshUserPrivateKey";
public static final String string = "string";

private Type() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key;

import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

import javax.annotation.Nonnull;

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsProvider;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.util.Secret;
import io.jenkins.plugins.credentials.secretsmanager.AwsCredentialsProvider;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import io.jenkins.plugins.credentials.secretsmanager.factory.BaseAwsJsonCredentials;

/**
* Similar to {@link AwsSshUserPrivateKey} but expects the AWS secret data to be
* JSON containing two or three fields, {@value #JSON_FIELDNAME_FOR_USERNAME},
* {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} and optionally a
* {@value #JSON_FIELDNAME_FOR_PASSPHRASE} that provide the username, key and
* (optional) passphrase. The secret JSON may contain other fields too, but
* we'll ignore them.
*/
public class AwsJsonSshUserPrivateKey extends BaseAwsJsonCredentials implements SSHUserPrivateKey {
/**
* Name of the JSON field that we expect to be present and to contain the
* credential's username.
*/
@Restricted(NoExternalUse.class)
public static final String JSON_FIELDNAME_FOR_USERNAME = "username";
/**
* Name of the JSON field that we expect to be present and to contain the
* credential's private key.
*/
@Restricted(NoExternalUse.class)
public static final String JSON_FIELDNAME_FOR_PRIVATE_KEY = "privatekey";
/**
* Name of the JSON field that we look for and, if present, expect it to contain
* the credential's password. If it isn't present then we assume no passphrase.
*/
@Restricted(NoExternalUse.class)
public static final String JSON_FIELDNAME_FOR_PASSPHRASE = "passphrase";

/**
* Constructs a new instance.
*
* @param id The value for {@link #getId()}.
* @param description The value for {@link #getDescription()}.
* @param json Supplies JSON containing a
* {@value #JSON_FIELDNAME_FOR_USERNAME} field, a
* {@value #JSON_FIELDNAME_FOR_PRIVATE_KEY} field and
* optionally a {@value #JSON_FIELDNAME_FOR_PASSPHRASE}
* field.
*/
public AwsJsonSshUserPrivateKey(String id, String description, Supplier<Secret> json) {
super(id, description, json);
}

/**
* Constructs a snapshot of an existing instance.
*
* @param toBeSnapshotted The instance that contains the live data to be
* snapshotted.
*/
@Restricted(NoExternalUse.class)
AwsJsonSshUserPrivateKey(AwsJsonSshUserPrivateKey toBeSnapshotted) {
super(toBeSnapshotted);
}

@NonNull
@Deprecated
@Override
public String getPrivateKey() {
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_PRIVATE_KEY);
}

@Override
public Secret getPassphrase() {
return Secret.fromString(getOptionalField(getSecretJson(), JSON_FIELDNAME_FOR_PASSPHRASE));
}

@NonNull
@Override
public List<String> getPrivateKeys() {
return Collections.singletonList(getPrivateKey());
}

@NonNull
@Override
public String getUsername() {
return getMandatoryField(getSecretJson(), JSON_FIELDNAME_FOR_USERNAME);
}

@Override
public boolean isUsernameSecret() {
return true;
}

@Extension
@SuppressWarnings("unused")
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
@Override
@Nonnull
public String getDisplayName() {
return Messages.sshUserPrivateKey();
}

@Override
public String getIconClassName() {
return "icon-ssh-credentials-ssh-key";
}

@Override
public boolean isApplicable(CredentialsProvider provider) {
return provider instanceof AwsCredentialsProvider;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.jenkins.plugins.credentials.secretsmanager.factory.ssh_user_private_key;

import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
import hudson.Extension;
import io.jenkins.plugins.credentials.secretsmanager.factory.Snapshot;

@Extension
@SuppressWarnings("unused")
public class AwsJsonSshUserPrivateKeySnapshotTaker extends CredentialsSnapshotTaker<AwsJsonSshUserPrivateKey> {
@Override
public Class<AwsJsonSshUserPrivateKey> type() {
return AwsJsonSshUserPrivateKey.class;
}

@Override
public AwsJsonSshUserPrivateKey snapshot(AwsJsonSshUserPrivateKey credential) {
return new AwsJsonSshUserPrivateKey(credential);
}
}
Loading