Skip to content

Commit

Permalink
Merge pull request jenkinsci#48 from fowlie/feature/JENKINS-53105
Browse files Browse the repository at this point in the history
[JENKINS-53105] Option for system scoped credentials
  • Loading branch information
jtnord authored Feb 9, 2021
2 parents 416890d + d04548e commit 85c9eb4
Show file tree
Hide file tree
Showing 24 changed files with 369 additions and 34 deletions.
1 change: 1 addition & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Credentials are added and updated by adding/updating them as secrets to Kubernet
The format of the Secret is different depending on the type of credential you wish to expose, but will all have several things in common:

- the label `"jenkins.io/credentials-type"` with a type that is known to the plugin (e.g. `certificate`, `secretFile`, `secretText`, `usernamePassword`, `basicSSHUserPrivateKey`, `aws`, 'openstackCredentialv3')
- the label `"jenkins.io/credentials-scope"` with a type that is either `global` (default) or `system`
- an annotation for the credential description: `"jenkins.io/credentials-description" : "certificate credential from Kubernetes"`

To add or update a Credential just execute the command `kubectl apply -f <nameOfFile.yaml>`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Optional;

import com.cloudbees.plugins.credentials.CredentialsScope;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
Expand All @@ -54,9 +57,11 @@ public abstract class SecretUtils {

/** Annotation prefix for the optional custom mapping of data */
private static final String JENKINS_IO_CREDENTIALS_KEYBINDING_ANNOTATION_PREFIX = "jenkins.io/credentials-keybinding-";

static final String JENKINS_IO_CREDENTIALS_TYPE_LABEL = "jenkins.io/credentials-type";

static final String JENKINS_IO_CREDENTIALS_SCOPE_LABEL = "jenkins.io/credentials-scope";


/**
* Convert a String representation of the base64 encoded bytes of a UTF-8 String back to a String.
Expand Down Expand Up @@ -97,6 +102,26 @@ public static byte[] base64Decode(String s) {
return null;
}

/**
* Get the scope from a given {@code Secret}.
* If the label is empty, then it defaults to global scope.
* @param s the secret whose scope we want to obtain.
* @return the scope for a given secret.
* @throws CredentialsConvertionException if scope is invalid.
*/
public static CredentialsScope getCredentialScope(Secret s) throws CredentialsConvertionException {
CredentialsScope scope = CredentialsScope.GLOBAL;
String label = s.getMetadata().getLabels().get(JENKINS_IO_CREDENTIALS_SCOPE_LABEL);
if (label != null) {
try {
scope = CredentialsScope.valueOf(label.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
throw new CredentialsConvertionException(JENKINS_IO_CREDENTIALS_SCOPE_LABEL + " is set to an invalid scope: " + label, exception);
}
}
return scope;
}

/**
* Obtain the credential ID from a given {@code Secret}.
* @param s the secret whose id we want to obtain.
Expand Down Expand Up @@ -127,7 +152,7 @@ public static String getCredentialDescription(Secret s) {
* @param obj the Object to check for {@code null}.
* @param exceptionMessage detail message to be used in the event that a CredentialsConvertionException is thrown.
* @param <T> the type of the obj.
* @return {@code obj} if not {@code null}.
* @return {@code obj} if not {@code null}.
* @throws CredentialsConvertionException iff {@code obj} is {@code null}.
*/
public static <T> T requireNonNull(@Nullable T obj, String exceptionMessage) throws CredentialsConvertionException {
Expand All @@ -143,7 +168,7 @@ public static <T> T requireNonNull(@Nullable T obj, String exceptionMessage) thr
* @param exceptionMessage detail message to be used in the event that a CredentialsConvertionException is thrown.
* @param mapped an optional mapping (adds a {@code "mapped to " + mapped} to the exception message if this is non null.
* @param <T> the type of the obj.
* @return {@code obj} if not {@code null}.
* @return {@code obj} if not {@code null}.
* @throws CredentialsConvertionException iff {@code obj} is {@code null}.
*/
public static <T> T requireNonNull(@Nullable T obj, String exceptionMessage, @Nullable String mapped) throws CredentialsConvertionException {
Expand All @@ -156,11 +181,11 @@ public static <T> T requireNonNull(@Nullable T obj, String exceptionMessage, @Nu
return obj;
}


/**
* Get the data for the specified key (or the mapped key if key is mapped), or throw a
* CredentialsConvertionException if the data for the given key was not present..
*
*
* @param s the Secret
* @param key the key to get the data for (which may be mapped to another key).
* @param exceptionMessage the detailMessage of the exception if the data for the key (or mapped key) was not
Expand All @@ -171,7 +196,7 @@ public static <T> T requireNonNull(@Nullable T obj, String exceptionMessage, @Nu
@SuppressFBWarnings(value= {"ES_COMPARING_PARAMETER_STRING_WITH_EQ"}, justification="the string will be the same string if not mapped")
public static String getNonNullSecretData(Secret s, String key, String exceptionMessage) throws CredentialsConvertionException {
String mappedKey = getKeyName(s, key);
if (mappedKey == key) { // use String == as getKeyName(key) will return key if no custom mapping is defined)
if (mappedKey == key) { // use String == as getKeyName(key) will return key if no custom mapping is defined)
return requireNonNull(s.getData().get(key), exceptionMessage, null);
}
return requireNonNull(s.getData().get(mappedKey), exceptionMessage, mappedKey);
Expand Down Expand Up @@ -199,7 +224,7 @@ public static Optional<String> getOptionalSecretData(Secret s, String key, Strin
* Get the mapping for the specified key name. Secrets can override the defaults used by the plugin by specifying an
* attribute of the type {@code jenkins.io/credentials-keybinding-name} containing the custom name - for example
* {@code jenkins.io/credentials-keybinding-foo=wibble}.
*
*
* @param s the secret to inspect for a custom name.
* @param key the name of the key we are looking for.
* @return the custom mapping for the key or {@code key} (identical object) if there is no custom mapping.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;

/**
* SecretToCredentialConvertor that converts {@link AWSCredentialsImpl}.
Expand Down Expand Up @@ -72,7 +71,7 @@ public AWSCredentialsImpl convert(Secret secret) throws CredentialsConvertionExc

return new AWSCredentialsImpl(
// Scope
CredentialsScope.GLOBAL,
SecretUtils.getCredentialScope(secret),
// ID
SecretUtils.getCredentialId(secret),
// AccessKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;

/**
* SecretToCredentialConvertor that converts {@link BasicSSHUserPrivateKey}.
Expand Down Expand Up @@ -63,7 +62,7 @@ public BasicSSHUserPrivateKey convert(Secret secret) throws CredentialsConvertio

return new BasicSSHUserPrivateKey(
// Scope
CredentialsScope.GLOBAL,
SecretUtils.getCredentialScope(secret),
// ID
SecretUtils.getCredentialId(secret),
// Username
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SecretBytes;
import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl;

Expand Down Expand Up @@ -58,7 +57,7 @@ public CertificateCredentialsImpl convert(Secret secret) throws CredentialsConve
byte[] certData = SecretUtils.requireNonNull(SecretUtils.base64Decode(certBase64), "certificate credential has an invalid certificate (must be base64 encoded data)");
SecretBytes sb = SecretBytes.fromBytes(certData);

CertificateCredentialsImpl certificateCredentialsImpl = new CertificateCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), password, new CertificateCredentialsImpl.UploadedKeyStoreSource(sb));
CertificateCredentialsImpl certificateCredentialsImpl = new CertificateCredentialsImpl(SecretUtils.getCredentialScope(secret), SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), password, new CertificateCredentialsImpl.UploadedKeyStoreSource(sb));
try {
if (certificateCredentialsImpl.getKeyStore().size() == 0) {
throw new CredentialsConvertionException("certificate credential has an invalid certificate (encoded data is not a valid PKCS#12 format certificate understood by Java)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.SecretBytes;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;

Expand Down Expand Up @@ -58,7 +57,7 @@ public FileCredentialsImpl convert(Secret secret) throws CredentialsConvertionEx
byte[] _data = SecretUtils.requireNonNull(SecretUtils.base64Decode(dataBase64), "secretFile credential has an invalid data (must be base64 encoded data)");

SecretBytes sb = SecretBytes.fromBytes(_data);
return new FileCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), filename, sb);
return new FileCredentialsImpl(SecretUtils.getCredentialScope(secret), SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), filename, sb);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;

import io.fabric8.kubernetes.api.model.Secret;

Expand Down Expand Up @@ -65,7 +64,7 @@ public OpenstackCredentialv3 convert(Secret secret) throws CredentialsConvertion
String passwordBase64 = SecretUtils.getNonNullSecretData(secret, "password", "openstackCredentialv3 credential is missing the password");
String password = SecretUtils.requireNonNull(SecretUtils.base64DecodeToString(passwordBase64), "openstackCredentialv3 credential has an invalid password (must be base64 encoded UTF-8)");

return new OpenstackCredentialv3(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), userName, userDomain, projectName, projectDomain, password);
return new OpenstackCredentialv3(SecretUtils.getCredentialScope(secret), SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), userName, userDomain, projectName, projectDomain, password);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;

/**
Expand All @@ -52,7 +51,7 @@ public StringCredentialsImpl convert(Secret secret) throws CredentialsConvertion

String secretText = SecretUtils.requireNonNull(SecretUtils.base64DecodeToString(textBase64), "secretText credential has an invalid text (must be base64 encoded UTF-8)");

return new StringCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), hudson.util.Secret.fromString(secretText));
return new StringCredentialsImpl(SecretUtils.getCredentialScope(secret), SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), hudson.util.Secret.fromString(secretText));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretToCredentialConverter;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;

/**
Expand All @@ -54,7 +53,7 @@ public UsernamePasswordCredentialsImpl convert(Secret secret) throws Credentials

String password = SecretUtils.requireNonNull(SecretUtils.base64DecodeToString(passwordBase64), "usernamePassword credential has an invalid password (must be base64 encoded UTF-8)");

return new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), username, password);
return new UsernamePasswordCredentialsImpl(SecretUtils.getCredentialScope(secret), SecretUtils.getCredentialId(secret), SecretUtils.getCredentialDescription(secret), username, password);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;

import com.cloudbees.plugins.credentials.CredentialsScope;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import org.junit.Test;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.CredentialsConvertionException;
import com.cloudbees.jenkins.plugins.kubernetes_credentials_provider.SecretUtils;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.text.StringContainsInOrder.stringContainsInOrder;
Expand Down Expand Up @@ -70,6 +72,20 @@ public void base64DecodeWithValidInput() {
assertThat(SecretUtils.base64Decode("SGVsbG8"), is(expected));
}

@Test
public void getCredentialScopeWithValidScopeLabel() throws CredentialsConvertionException {
Map<String, String> scopeLabel = Collections.singletonMap(SecretUtils.JENKINS_IO_CREDENTIALS_SCOPE_LABEL, "system");
Secret s = new SecretBuilder().withNewMetadata().withLabels(scopeLabel).endMetadata().build();
assertThat(SecretUtils.getCredentialScope(s), is(CredentialsScope.SYSTEM));
}

@Test(expected = CredentialsConvertionException.class)
public void getCredentialScopeWithInvalidScopeThrowsAnException() throws CredentialsConvertionException {
Map<String, String> invalidScope = Collections.singletonMap(SecretUtils.JENKINS_IO_CREDENTIALS_SCOPE_LABEL, "barf");
Secret secret = new SecretBuilder().withNewMetadata().withLabels(invalidScope).endMetadata().build();
SecretUtils.getCredentialScope(secret);
}

@Test
public void getCredentialId() {
final String testName = "a-test-name";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import io.fabric8.kubernetes.client.utils.Serialization;
import com.cloudbees.jenkins.plugins.awscredentials.AWSCredentialsImpl;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;

import java.io.InputStream;

Expand Down Expand Up @@ -164,6 +165,26 @@ public void canConvertAValidSecretWithNoIamMfa() throws Exception {
}
}

@Issue("JENKINS-53105")
@Test
public void canConvertAValidScopedSecret() throws Exception {
AWSCredentialsConvertor convertor = new AWSCredentialsConvertor();

try (InputStream is = get("validScoped.yaml")) {
Secret secret = Serialization.unmarshal(is, Secret.class);
assertThat("The Secret was loaded correctly from disk", notNullValue());
AWSCredentialsImpl credential = convertor.convert(secret);
assertThat(credential, notNullValue());
assertThat("credential id is mapped correctly", credential.getId(), is("a-test-aws"));
assertThat("credential description is mapped correctly", credential.getDescription(), is("credentials from Kubernetes"));
assertThat("credential scope is mapped correctly", credential.getScope(), is(CredentialsScope.SYSTEM));
assertThat("credential accessKey is mapped correctly", credential.getAccessKey(), is(accessKey));
assertThat("credential secretKey is mapped correctly", credential.getSecretKey().getPlainText(), is(secretKey));
assertThat("credential iamRoleArn is mapped correctly", credential.getIamRoleArn(), is(iamRoleArn));
assertThat("credential iamMfaSerialNumber is mapped correctly", credential.getIamMfaSerialNumber(), is(iamMfaSerialNumber));
}
}

@Test
public void failsToConvertWhenAccessKeyMissing() throws Exception {
AWSCredentialsConvertor convertor = new AWSCredentialsConvertor();
Expand Down
Loading

0 comments on commit 85c9eb4

Please sign in to comment.