Skip to content

Commit

Permalink
Add support for scoping credentials to SYSTEM (#222)
Browse files Browse the repository at this point in the history
  • Loading branch information
bsloan-icl authored Oct 19, 2023
1 parent 03849ad commit a31ba45
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 16 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,15 +422,15 @@ az keyvault secret set --vault-name my-vault \

With the System Property or Environment variable being set in this example, only the usernamePassword `testUserWithLabel` will be present in your Jenkins instance.

#### Jenkins Credential ID and Description
#### Jenkins Credential ID, Description and Scope

The ID and description of credentials can be specified with tags on the Azure Key Vault secret. The ID is specified with the tag "jenkinsID" and appears in the Jenkins credentials UI in the "ID" column. This is the credentials ID parameter used in `withCredentials()` calls. The description is specified with the tag "description" and appears in Jenkins credentials UI in the "Name" column.
The ID, description and scope of credentials can be specified with tags on the Azure Key Vault secret. The ID is specified with the tag "jenkinsID" and appears in the Jenkins credentials UI in the "ID" column. This is the credentials ID parameter used in `withCredentials()` calls. The description is specified with the tag "description" and appears in the Jenkins credentials UI in the "Name" column. The scope is specified with the tag "scope" and can be set to "system" or "global". By default the scope will be set to global.

```bash
az keyvault secret set --vault-name my-vault \
--name testUserWithLabel \
--value example2 \
--tags jenkinsID=myCred description="This is my credential"
--tags jenkinsID=myCred description="This is my credential" scope=system
```

### SecretSource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
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.cloudbees.plugins.credentials.domains.DomainRequirement;
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 edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.Extension;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.security.ACL;
Expand All @@ -27,6 +30,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand All @@ -45,6 +49,7 @@ public class AzureCredentialsProvider extends CredentialsProvider {

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

private final AzureCredentialsStore store = new AzureCredentialsStore(this);

Expand All @@ -57,7 +62,6 @@ public class AzureCredentialsProvider extends CredentialsProvider {
public void refreshCredentials() {
cache.invalidateAll();
}

@NonNull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> aClass, @Nullable ItemGroup itemGroup,
Expand All @@ -72,10 +76,15 @@ public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> aClass,

for (IdCredentials credential : credentials) {
if (aClass.isAssignableFrom(credential.getClass())) {
// cast to keep generics happy even though we are assignable
list.add(aClass.cast(credential));
if (CredentialsScope.SYSTEM == credential.getScope() && !(itemGroup instanceof Jenkins)) {
LOG.log(Level.FINEST, "getCredentials {0} has SYSTEM scope but the context is not Jenkins. Ignoring credential", credential.getId());
} else if (aClass.isAssignableFrom(credential.getClass())) {
// cast to keep generics happy even though we are assignable
list.add(aClass.cast(credential));
} else {
LOG.log(Level.FINEST, "getCredentials {0} does not match", credential.getId());
}
}
LOG.log(Level.FINEST, "getCredentials {0} does not match", credential.getId());
}
} catch (RuntimeException e) {
LOG.log(Level.WARNING, "Error retrieving secrets from Azure KeyVault: " + e.getMessage(), e);
Expand All @@ -87,6 +96,25 @@ public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> aClass,
return Collections.emptyList();
}

@Override
@NonNull
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type,
@NonNull Item item,
Authentication authentication) {
// scoping to Items is not supported so using null to not expose SYSTEM credentials to Items.
Objects.requireNonNull(item);
return getCredentials(type, (ItemGroup)null, authentication);
}

@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type,
@NonNull Item item,
Authentication authentication,
List<DomainRequirement> domainRequirements) {
// domain requirements not supported
return getCredentials(type, item, authentication);
}

@VisibleForTesting
static String getSecretName(String itemId) {
if (StringUtils.isEmpty(itemId)) {
Expand Down Expand Up @@ -136,6 +164,13 @@ private static Collection<IdCredentials> fetchCredentials() {
String type = tags.getOrDefault("type", DEFAULT_TYPE);
String jenkinsID = tags.getOrDefault("jenkinsID", getSecretName(id));
String description = tags.getOrDefault("description", "");
String labelScope = tags.getOrDefault("scope", DEFAULT_SCOPE).toUpperCase();

CredentialsScope scope = CredentialsScope.GLOBAL;

if (tags.containsKey("scope") && labelScope.equals("SYSTEM")) {
scope = CredentialsScope.SYSTEM;
}

// initial implementation didn't require a type
if (tags.containsKey("username") && type.equals(DEFAULT_TYPE)) {
Expand All @@ -144,13 +179,13 @@ private static Collection<IdCredentials> fetchCredentials() {

switch (type) {
case "string": {
AzureSecretStringCredentials cred = new AzureSecretStringCredentials(jenkinsID, description, new KeyVaultSecretRetriever(client, id));
AzureSecretStringCredentials cred = new AzureSecretStringCredentials(scope, jenkinsID, description, new KeyVaultSecretRetriever(client, id));
credentials.add(cred);
break;
}
case "username": {
AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials(
jenkinsID, tags.get("username"), description, new KeyVaultSecretRetriever(client, id)
scope, jenkinsID, tags.get("username"), description, new KeyVaultSecretRetriever(client, id)
);
credentials.add(cred);
break;
Expand All @@ -173,7 +208,7 @@ private static Collection<IdCredentials> fetchCredentials() {

}
AzureSSHUserPrivateKeyCredentials cred = new AzureSSHUserPrivateKeyCredentials(
jenkinsID, description, tags.get("username"), usernameSecret, passphrase, new KeyVaultSecretRetriever(client, id)
scope, jenkinsID, description, tags.get("username"), usernameSecret, passphrase, new KeyVaultSecretRetriever(client, id)

Check warning on line 211 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 79-211 are not covered by tests
);
credentials.add(cred);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.jenkins.plugins.sshcredentials.impl.Messages;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
Expand All @@ -24,14 +25,15 @@ public class AzureSSHUserPrivateKeyCredentials extends BaseStandardCredentials i
private final Secret passphrase;

public AzureSSHUserPrivateKeyCredentials(
CredentialsScope scope,
String id,
String description,
String username,
boolean usernameSecret,
Secret passphrase,
Supplier<Secret> privateKey
) {
super(id, description);
super(scope, id, description);

Check warning on line 36 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentials.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 36 is not covered by tests
this.username = username;
this.usernameSecret = usernameSecret;
this.passphrase = passphrase;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public Class<AzureSSHUserPrivateKeyCredentials> type() {
public AzureSSHUserPrivateKeyCredentials snapshot(AzureSSHUserPrivateKeyCredentials credential) {
SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getSecretValue());
return new AzureSSHUserPrivateKeyCredentials(
credential.getScope(),

Check warning on line 20 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentialsSnapshotTaker.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 20 is not covered by tests
credential.getId(),
credential.getDescription(),
credential.getUsername(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
Expand All @@ -15,8 +16,8 @@ public class AzureSecretStringCredentials extends BaseStandardCredentials implem

private final Supplier<Secret> value;

public AzureSecretStringCredentials(String id, String description, Supplier<Secret> value) {
super(id, description);
public AzureSecretStringCredentials(CredentialsScope scope, String id, String description, Supplier<Secret> value) {
super(scope, id, description);

Check warning on line 20 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentials.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 20 is not covered by tests
this.value = value;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public Class<AzureSecretStringCredentials> type() {
@Override
public AzureSecretStringCredentials snapshot(AzureSecretStringCredentials credential) {
SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getSecret());
return new AzureSecretStringCredentials(credential.getId(), credential.getDescription(), secretSnapshot);
return new AzureSecretStringCredentials(credential.getScope(), credential.getId(), credential.getDescription(), secretSnapshot);

Check warning on line 19 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/string/AzureSecretStringCredentialsSnapshotTaker.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 19 is not covered by tests
}

private static class SecretSnapshot extends Snapshot<Secret> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import com.cloudbees.plugins.credentials.impl.Messages;
Expand All @@ -18,12 +19,13 @@ public class AzureUsernamePasswordCredentials extends BaseStandardCredentials im
private final String username;

public AzureUsernamePasswordCredentials(
CredentialsScope scope,
String id,
String username,
String description,
Supplier<Secret> password
) {
super(id, description);
super(scope, id, description);

Check warning on line 28 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentials.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 28 is not covered by tests
this.password = password;
this.username = Util.fixNull(username);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public Class<AzureUsernamePasswordCredentials> type() {
@Override
public AzureUsernamePasswordCredentials snapshot(AzureUsernamePasswordCredentials credential) {
SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getPassword());
return new AzureUsernamePasswordCredentials(credential.getId(), credential.getUsername(), credential.getDescription(), secretSnapshot);
return new AzureUsernamePasswordCredentials(credential.getScope(), credential.getId(), credential.getUsername(), credential.getDescription(), secretSnapshot);

Check warning on line 19 in src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/usernamepassword/AzureUsernamePasswordCredentialsSnapshotTaker.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 19 is not covered by tests
}

private static class SecretSnapshot extends Snapshot<Secret> {
Expand Down

0 comments on commit a31ba45

Please sign in to comment.