Skip to content

Commit

Permalink
Snapshot credentials so they work on agents (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja authored Oct 29, 2021
1 parent be94436 commit 867845e
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ buildNumber.properties
!/.mvn/wrapper/maven-wrapper.jar
work/
*.dylib
deployment.out
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,53 @@
import com.azure.security.keyvault.secrets.models.SecretProperties;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import com.microsoft.jenkins.keyvault.SecretClientCache;
import com.microsoft.jenkins.keyvault.SecretStringCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.security.ACL;
import hudson.util.Secret;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.GlobalConfiguration;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.lang3.StringUtils;
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials;
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword.AzureUsernamePasswordCredentials;


@Extension
public class AzureCredentialsProvider extends CredentialsProvider {
private static final Logger LOG = Logger.getLogger(AzureCredentialsProvider.class.getName());

private static final String CACHE_KEY = "key";
private static final String DEFAULT_TYPE = "string";

private final AzureCredentialsStore store = new AzureCredentialsStore(this);

private final LoadingCache<String, Collection<IdCredentials>> cache = Caffeine.newBuilder()
.maximumSize(1L)
.expireAfterWrite(Duration.ofMinutes(120))
.refreshAfterWrite(Duration.ofMinutes(10))
.build(key -> fetchCredentials());
.maximumSize(1L)
.expireAfterWrite(Duration.ofMinutes(120))
.refreshAfterWrite(Duration.ofMinutes(10))
.build(key -> fetchCredentials());

public void refreshCredentials() {
cache.refresh(CACHE_KEY);
Expand Down Expand Up @@ -93,7 +99,7 @@ static String getSecretName(String itemId) {

private static Collection<IdCredentials> fetchCredentials() {
AzureKeyVaultGlobalConfiguration azureKeyVaultGlobalConfiguration = GlobalConfiguration.all()
.get(AzureKeyVaultGlobalConfiguration.class);
.get(AzureKeyVaultGlobalConfiguration.class);
if (azureKeyVaultGlobalConfiguration == null) {
throw new AzureKeyVaultException("No global key vault url configured.");
}
Expand All @@ -106,14 +112,34 @@ private static Collection<IdCredentials> fetchCredentials() {
for (SecretProperties secretItem : client.listPropertiesOfSecrets()) {
String id = secretItem.getId();
Map<String, String> tags = secretItem.getTags();
if (tags != null && tags.containsKey("username")) {
AzureKeyVaultUsernamePasswordCredentials cred = new AzureKeyVaultUsernamePasswordCredentials(
CredentialsScope.GLOBAL, getSecretName(id), tags.get("username"), id, credentialID, id
);
credentials.add(cred);
} else {
SecretStringCredentials cred = new SecretStringCredentials(CredentialsScope.GLOBAL, getSecretName(id), id, credentialID, id);
credentials.add(cred);

if (tags == null) {
tags = new HashMap<>();
}

String type = tags.getOrDefault("type", DEFAULT_TYPE);

// initial implementation didn't require a type
if (tags.containsKey("username") && type.equals(DEFAULT_TYPE)) {
type = "username";
}

switch (type) {
case "string": {
AzureSecretStringCredentials cred = new AzureSecretStringCredentials(getSecretName(id), "", new KeyVaultSecretRetriever(client, id));
credentials.add(cred);
}
break;
case "username": {
AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials(
getSecretName(id), tags.get("username"), "", new KeyVaultSecretRetriever(client, id)
);
credentials.add(cred);
}
break;
default: {
throw new IllegalStateException("Unknown type: " + type);
}
}
}
return credentials;
Expand All @@ -123,6 +149,43 @@ CredentialsScope.GLOBAL, getSecretName(id), tags.get("username"), id, credential
}
}

private static class KeyVaultSecretRetriever implements Supplier<Secret> {

private final transient SecretClient client;
private final String secretId;

public KeyVaultSecretRetriever(SecretClient secretClient, String secretId) {
this.client = secretClient;
this.secretId = secretId;
}

public String retrieveSecret() {
int NAME_POSITION = 2;
int VERSION_POSITION = 3;
URL secretIdentifierUrl;
try {
secretIdentifierUrl = new URL(secretId);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}

// old SDK supports secret identifier which is a full URI to the secret
// the new SDK doesn't seem to support it to we parse it to get the values we need
// https://mine.vault.azure.net/secrets/<name>/<version>
String[] split = secretIdentifierUrl.getPath().split("/");

if (split.length == NAME_POSITION + 1) {
return client.getSecret(split[NAME_POSITION]).getValue();
}
return client.getSecret(split[NAME_POSITION], split[VERSION_POSITION]).getValue();
}

@Override
public Secret get() {
return Secret.fromString(retrieveSecret());
}
}

@Override
public CredentialsStore getStore(ModelObject object) {
return object == Jenkins.get() ? store : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials;

import java.io.Serializable;
import java.util.function.Supplier;

public class Snapshot<T> implements Supplier<T>, Serializable {

private static final long serialVersionUID = 1L;

private final T value;

public Snapshot(T value) {
this.value = value;
}

@Override
public T get() {
return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.util.Secret;
import java.util.function.Supplier;
import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.jenkinsci.plugins.plaincredentials.impl.Messages;
import org.jvnet.localizer.ResourceBundleHolder;

public class AzureSecretStringCredentials extends BaseStandardCredentials implements StringCredentials {

private final Supplier<Secret> value;

public AzureSecretStringCredentials(String id, String description, Supplier<Secret> value) {
super(id, description);
this.value = value;
}

@NonNull
@Override
public Secret getSecret() {
return value.get();
}

@Extension
@SuppressWarnings("unused")
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {
@Override
@NonNull
public String getDisplayName() {
return ResourceBundleHolder.get(Messages.class).format("StringCredentialsImpl.secret_text");
}

@Override
public boolean isApplicable(CredentialsProvider provider) {
return provider instanceof AzureCredentialsProvider;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string;

import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
import hudson.Extension;
import hudson.util.Secret;
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot;

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

@Override
public AzureSecretStringCredentials snapshot(AzureSecretStringCredentials credential) {
SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getSecret());
return new AzureSecretStringCredentials(credential.getId(), credential.getDescription(), secretSnapshot);
}

private static class SecretSnapshot extends Snapshot<Secret> {
SecretSnapshot(Secret value) {
super(value);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
package org.jenkinsci.plugins.azurekeyvaultplugin;
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword;

import com.azure.security.keyvault.secrets.models.KeyVaultSecret;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.cloudbees.plugins.credentials.impl.Messages;
import com.microsoft.jenkins.keyvault.BaseSecretCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
import hudson.util.Secret;
import java.util.function.Supplier;
import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider;
import org.jvnet.localizer.ResourceBundleHolder;


public class AzureKeyVaultUsernamePasswordCredentials extends BaseSecretCredentials implements StandardUsernamePasswordCredentials {
final protected CredentialsScope scope;
final protected String username;
public class AzureUsernamePasswordCredentials extends BaseStandardCredentials implements StandardUsernamePasswordCredentials {
private final Supplier<Secret> password;
private final String username;


public AzureKeyVaultUsernamePasswordCredentials(
CredentialsScope scope,
public AzureUsernamePasswordCredentials(
String id,
String username,
String description,
String servicePrincipalId,
String secretIdentifier) {
super(scope, id, description, servicePrincipalId, secretIdentifier);
this.scope = scope;
Supplier<Secret> password
) {
super(id, description);
this.password = password;
this.username = Util.fixNull(username);
}

@NonNull
@Override
public Secret getPassword() {
final KeyVaultSecret secretBundle = getKeyVaultSecret();
return Secret.fromString(secretBundle.getValue());
return password.get();
}

@NonNull
Expand All @@ -42,11 +40,6 @@ public String getUsername() {
return this.username;
}

@Override
public CredentialsScope getScope() {
return this.scope;
}

@Extension(ordinal = 1)
public static class DescriptorImpl extends BaseStandardCredentialsDescriptor {

Expand All @@ -66,5 +59,10 @@ public String getDisplayName() {
public String getIconClassName() {
return "icon-credentials-userpass";
}

@Override
public boolean isApplicable(CredentialsProvider provider) {
return provider instanceof AzureCredentialsProvider;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword;

import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker;
import hudson.Extension;
import hudson.util.Secret;
import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot;

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

@Override
public AzureUsernamePasswordCredentials snapshot(AzureUsernamePasswordCredentials credential) {
SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getPassword());
return new AzureUsernamePasswordCredentials(credential.getId(), credential.getUsername(), credential.getDescription(), secretSnapshot);
}

private static class SecretSnapshot extends Snapshot<Secret> {
SecretSnapshot(Secret value) {
super(value);
}
}
}

0 comments on commit 867845e

Please sign in to comment.