Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow credential id to be specified #53

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
10 changes: 9 additions & 1 deletion docs/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,16 @@ Only AWS AccessKey and SecretKey:

Sometimes you may want the secret to be able to be consumed by another tool as well that has a different requirement for the data fields.
In order to facilitate this the plugin supports the remapping fields.
In order to achieve this you add an attribute begining with `jenkins.io/credentials-keybinding-` and ending with the normal field name and having the value of the new field name.
In order to achieve this you add an attribute beginning with `jenkins.io/credentials-keybinding-` and ending with the normal field name and having the value of the new field name.
The following example remaps the `username` and `password` fields to `user` and `pass`:
{% highlight yaml linenos %}
{% include_relative username-pass-with-custom-mapping.yaml %}
{% endhighlight %}

# Overriding the Credential Name

By default, the name of the `Secret` will be used as the name of the credential, but as Kubernetes only allows valid DNS names as `Secret` names you may want to override this behaviour.
In order to achieve this you need to add a label to the `Secret` with the name `jenkins.io/credentials-id` and the value of the credential name you wish to configure.
{% highlight yaml linenos %}
{% include_relative username-pass-with-name-override.yaml %}
{% endhighlight %}
17 changes: 17 additions & 0 deletions docs/examples/username-pass-with-name-override.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Secret
metadata:
# this is the jenkins id.
name: "another-test-usernamepass"
labels:
# so we know what type it is.
"jenkins.io/credentials-type": "usernamePassword"
# override the credential id
"jenkins.io/credentials-id": "CUSTOM_ID_OF_CREDENTIAL"
annotations:
# description - can not be a label as spaces are not allowed
"jenkins.io/credentials-description" : "credentials from Kubernetes"
type: Opaque
stringData:
username: myUsername
password: 'Pa$$word'
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ 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_ID_LABEL = "jenkins.io/credentials-id";

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

static final String JENKINS_IO_CREDENTIALS_SCOPE_LABEL = "jenkins.io/credentials-scope";
Expand Down Expand Up @@ -128,6 +130,13 @@ public static CredentialsScope getCredentialScope(Secret s) throws CredentialsCo
* @return the credential ID for a given secret.
*/
public static String getCredentialId(Secret s) {
Map<String, String> labels = s.getMetadata().getLabels();
if (labels != null) {
String overrideId = labels.get(JENKINS_IO_CREDENTIALS_ID_LABEL);
if (overrideId != null) {
return overrideId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What stops 2 credentials with the same label value?
What stops one with the same value as another secrets name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, is there anything stopping someone from creating a Secret with the same name as an existing credential?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could store a mapping of Secret name -> Credential ID and warn/error if there are conflicts / or changes that need to be made?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you mean an ID "foo" in k8s and using the stock credentials provider then no - but you can not do that twice in the same provider.
When you do manage to do it it is always deterministic, you get the Credential (Secret) from the credential provider with the highest ordinal (and if there are multiple with the same ordinal it is backed by a list so deterministic until you restart, and then it is mostly stable as it comes from the plugin scout order).

With this approach, if one secret got updated it would be used until the other secret got updated, causing potential randomness. I guess k8s list uses some stable ordering but if not, just reconnecting to the API server after a connection will potentially cause changes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could store a mapping of Secret name -> Credential ID and warn/error if there are conflicts / or changes that need to be made?

We could, but at the end of the day I think credential aliasing would be much more useful. At the end of the day - the IDs are not needed to interact with any external system - so this should only ever be an interim solution (e.g. migration) and as such not a core part of the plugin (you may want to start the migration before you move to k8s)...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I agree, from the Jenkins docs on https://www.jenkins.io/doc/book/using/using-credentials/ it states:

"You can use upper- or lower-case letters for the credential ID, as well as any valid separator character. However, for the benefit of all users on your Jenkins instance, it is best to use a single and consistent convention for specifying credential IDs."

It should be possible to do this without the need for an additional plugin (which doesn't yet exist).

Copy link
Member

@jtnord jtnord Jun 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That talks about the stock credentials provider only. Other credentials provider (such as cyberark) and k8s may well have their own further restrictions. I will file a PR to get that page fixed :)

update -> jenkins-infra/jenkins.io#4390

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have no intention of merging this, could you mark the JIRA issues as Won't Fix to make sure that this PR is not opened for the third time.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a pity to refuse such feature for the reason of potential Bad usage by guys who have overall administrative rights.

Except if it compromise plugin stability or worse Jenkins instance, I see no reason to reject it.

I use a lot JasC and if I misconfigure my Jenkins instance. It is my owb ans entire fault. Features (i.e. Plugins) may limit misbehavior/configuration but must offer freedom in usage (at least for power user).

You made technical design decision that already affect usability and there is a simple solution to leverage it with low-risk cost (manageable and not affecting stability).

As I havent took a look at how it works not sure if label (use for identification and selection) is more appropriate than annotation (use for information).

Dont take my words too tough, english isnt my mother tongue. But I highly want you to reconsider your position regarding naming "freedom" (over just aliasing).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said in the Jira there is no need to support this in this plugin.
If you want to be able to migrate credentials then that can be done in a different plugin that supports credential aliasing and can work across any credential provider.

I'm not saying there is not a need for that, but I am saying I do not see a need to be able to specify a non DNS corformant id in this pkugin., The only reason anyone has put forward for that so far is migrating.

}
}
// we must have a metadata as the label that identifies this as a Jenkins credential needs to be present
return s.getMetadata().getName();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,15 @@ public void startWatchingForSecrets() {
Secret s1 = createSecret("s1");
Secret s2 = createSecret("s2");
Secret s3 = createSecret("s3");
Secret s4 = createSecretWithNameOverride("s4", "INVALID_SECRET_NAME");

// returns s1 and s3, the credentials map should be reset to this list
server.expect().withPath("/api/v1/namespaces/test/secrets?labelSelector=jenkins.io%2Fcredentials-type")
.andReturn(200, new SecretListBuilder()
.withNewMetadata()
.withResourceVersion("1")
.endMetadata()
.addToItems(s1, s3)
.addToItems(s1, s3, s4)
.build())
.once();

Expand All @@ -104,9 +105,10 @@ public void startWatchingForSecrets() {
provider.startWatchingForSecrets();

List<UsernamePasswordCredentials> credentials = provider.getCredentials(UsernamePasswordCredentials.class, (ItemGroup) null, ACL.SYSTEM);
assertEquals("credentials", 2, credentials.size());
assertEquals("credentials", 3, credentials.size());
assertTrue("secret s1 exists", credentials.stream().anyMatch(c -> "s1".equals(((UsernamePasswordCredentialsImpl) c).getId())));
assertTrue("secret s3 exists", credentials.stream().anyMatch(c -> "s3".equals(((UsernamePasswordCredentialsImpl) c).getId())));
assertTrue("secret s4 exists", credentials.stream().anyMatch(c -> "INVALID_SECRET_NAME".equals(((UsernamePasswordCredentialsImpl) c).getId())));
}

private Secret createSecret(String name) {
Expand All @@ -121,6 +123,19 @@ private Secret createSecret(String name) {
.build();
}

private Secret createSecretWithNameOverride(String name, String nameOverride) {
return new SecretBuilder()
.withNewMetadata()
.withNamespace("test")
.withName(name)
.addToLabels("jenkins.io/credentials-type", "usernamePassword")
.addToLabels("jenkins.io/credentials-id", nameOverride)
.endMetadata()
.addToData("username", "bXlVc2VybmFtZQ==")
.addToData("password", "UGEkJHdvcmQ=")
.build();
}

@Test
public void startWatchingForSecretsKubernetesClientException() throws IOException {
KubernetesCredentialProvider provider = new MockedKubernetesCredentialProvider();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public void getCredentialId() {
assertThat(SecretUtils.getCredentialId(s), is(testName));
}

@Test
public void getCredentialIdWithNameOverride() {
final String secretName = "a-test-name";
final String credentialName = "A_TEST_NAME";
Secret s = new SecretBuilder()
.withNewMetadata()
.withName(secretName)
.withLabels(Collections.singletonMap(SecretUtils.JENKINS_IO_CREDENTIALS_ID_LABEL, credentialName))
.endMetadata().build();
assertThat(SecretUtils.getCredentialId(s), is(credentialName));
}

@Test
public void getCredentialDescription() {
final String testDescription = "a-test-description";
Expand Down