Skip to content

Commit 05fd220

Browse files
committed
feat: add merge support for casc defined system credentials
Enables support for merging casc defined credentials with existing credentials (i.e. manually created). fixes JENKINS-64079
1 parent 839a635 commit 05fd220

File tree

5 files changed

+245
-3
lines changed

5 files changed

+245
-3
lines changed

src/main/java/com/cloudbees/plugins/credentials/SystemCredentialsProvider.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ public synchronized void setDomainCredentialsMap(Map<Domain, List<Credentials>>
186186
this.domainCredentialsMap = DomainCredentials.toCopyOnWriteMap(domainCredentialsMap);
187187
}
188188

189+
/**
190+
* Merge the given credentials with the current set. Replace existing domain credentials or add new credentials.
191+
* Existing credentials not in the given set will not be removed.
192+
*
193+
* @param domainCredentialsMap credentials to add or update
194+
*/
195+
public synchronized void mergeDomainCredentialsMap(Map<Domain, List<Credentials>> domainCredentialsMap) {
196+
for (Map.Entry<Domain, List<Credentials>> entry : DomainCredentials.toCopyOnWriteMap(domainCredentialsMap).entrySet()) {
197+
List<Credentials> target = this.domainCredentialsMap.get(entry.getKey());
198+
if (target == null) {
199+
this.domainCredentialsMap.put(entry.getKey(), entry.getValue());
200+
} else {
201+
target.removeAll(entry.getValue());
202+
target.addAll(entry.getValue());
203+
this.domainCredentialsMap.remove(entry.getKey());
204+
this.domainCredentialsMap.put(entry.getKey(), target);
205+
}
206+
}
207+
}
208+
189209
/**
190210
* Short-cut method for {@link Jenkins#checkPermission(hudson.security.Permission)}
191211
*

src/main/java/com/cloudbees/plugins/credentials/casc/SystemCredentialsProviderConfigurator.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import edu.umd.cs.findbugs.annotations.CheckForNull;
3131
import edu.umd.cs.findbugs.annotations.NonNull;
3232
import hudson.Extension;
33+
import hudson.Util;
3334
import io.jenkins.plugins.casc.Attribute;
3435
import io.jenkins.plugins.casc.BaseConfigurator;
3536
import io.jenkins.plugins.casc.ConfigurationContext;
@@ -39,11 +40,21 @@
3940
import org.kohsuke.accmod.Restricted;
4041
import org.kohsuke.accmod.restrictions.NoExternalUse;
4142

43+
import java.util.Arrays;
4244
import java.util.Collections;
45+
import java.util.HashSet;
46+
import java.util.Map;
4347
import java.util.Set;
4448

4549
/**
46-
* A configurator for system credentials provider located beneath the {@link CredentialsRootConfigurator}
50+
* A configurator for system credentials provider located beneath the {@link CredentialsRootConfigurator}. The default
51+
* merge strategy will replace all existing credentials. To merge CasC credentials with existing credentials use
52+
* the env var {@code CASC_CREDENTIALS_MERGE_STRATEGY} or system property {@code casc.credentials.merge.strategy}
53+
* to set the strategy to "{@code merge}". The "{@code merge}" strategy will not remove credentials don't exist in
54+
* CasC configuration.
55+
*
56+
* @see SystemCredentialsProvider#mergeDomainCredentialsMap(Map)
57+
* @see SystemCredentialsProvider#setDomainCredentialsMap(Map)
4758
*/
4859
@Extension(optional = true, ordinal = 2)
4960
@Restricted(NoExternalUse.class)
@@ -64,17 +75,29 @@ protected SystemCredentialsProvider instance(Mapping mapping, ConfigurationConte
6475
public Set<Attribute<SystemCredentialsProvider, ?>> describe() {
6576
return Collections.singleton(
6677
new MultivaluedAttribute<SystemCredentialsProvider, DomainCredentials>("domainCredentials", DomainCredentials.class)
67-
.setter( (target, value) -> target.setDomainCredentialsMap(DomainCredentials.asMap(value)))
78+
.setter((target, value) -> {
79+
String strategy = Util.fixEmptyAndTrim(
80+
System.getProperty("casc.credentials.merge.strategy",
81+
System.getenv("CASC_CREDENTIALS_MERGE_STRATEGY")
82+
));
83+
84+
if ("merge".equalsIgnoreCase(strategy)) {
85+
target.mergeDomainCredentialsMap(DomainCredentials.asMap(value));
86+
} else {
87+
target.setDomainCredentialsMap(DomainCredentials.asMap(value));
88+
}
89+
})
6890
);
6991
}
7092

7193
@CheckForNull
7294
@Override
7395
public CNode describe(SystemCredentialsProvider instance, ConfigurationContext context) throws Exception {
7496
Mapping mapping = new Mapping();
75-
for (Attribute attribute : describe()) {
97+
for (Attribute<SystemCredentialsProvider, ?> attribute : describe()) {
7698
mapping.put(attribute.getName(), attribute.describe(instance, context));
7799
}
78100
return mapping;
79101
}
102+
80103
}

src/test/java/com/cloudbees/plugins/credentials/SystemCredentialsProviderTest.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
package com.cloudbees.plugins.credentials;
2525

2626
import com.cloudbees.plugins.credentials.common.IdCredentials;
27+
import com.cloudbees.plugins.credentials.domains.Domain;
28+
import com.cloudbees.plugins.credentials.domains.DomainCredentials;
2729
import com.cloudbees.plugins.credentials.impl.DummyCredentials;
2830
import com.cloudbees.plugins.credentials.impl.DummyIdCredentials;
2931
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -39,10 +41,20 @@
3941
import hudson.tasks.BuildStepDescriptor;
4042
import hudson.tasks.Builder;
4143

44+
import java.util.Arrays;
45+
import java.util.Collections;
4246
import java.util.HashMap;
47+
import java.util.List;
48+
import java.util.Map;
49+
import java.util.Objects;
50+
import java.util.function.Function;
51+
import java.util.function.Supplier;
52+
import java.util.stream.Collectors;
53+
4354
import jenkins.security.QueueItemAuthenticatorConfiguration;
4455
import org.acegisecurity.Authentication;
4556
import org.apache.commons.io.FileUtils;
57+
import org.apache.commons.lang.StringUtils;
4658
import org.junit.Rule;
4759
import org.junit.Test;
4860
import org.jvnet.hudson.test.JenkinsRule;
@@ -51,6 +63,7 @@
5163
import org.jvnet.hudson.test.TestExtension;
5264
import org.kohsuke.stapler.DataBoundConstructor;
5365

66+
import static org.junit.Assert.assertEquals;
5467
import static org.junit.Assert.assertFalse;
5568
import static org.junit.Assert.assertNotNull;
5669
import static org.junit.Assert.assertTrue;
@@ -164,6 +177,72 @@ public void given_globalScopeCredential_when_builtAsUserWithoutUseItem_then_cred
164177
r.assertBuildStatus(Result.FAILURE, prj.scheduleBuild2(0).get());
165178
}
166179

180+
@Test
181+
public void mergeDomainCredentialsMap() {
182+
SystemCredentialsProvider provider = SystemCredentialsProvider.getInstance();
183+
184+
// initial creds
185+
Map<Domain, List<Credentials>> creds = new HashMap<>();
186+
creds.put(null, Arrays.asList(
187+
new DummyIdCredentials("foo-manchu", CredentialsScope.GLOBAL, "foo", "manchu", "Dr. Fu Manchu"),
188+
new DummyIdCredentials("bar-manchu", CredentialsScope.GLOBAL, "bar", "manchu", "Dr. Bar Manchu")
189+
));
190+
Domain catsDotCom = new Domain("cats.com", "cats dot com", Collections.emptyList());
191+
creds.put(catsDotCom, Arrays.asList(
192+
new DummyIdCredentials("kitty-cat", CredentialsScope.GLOBAL, "kitty", "manchu", "Mrs. Kitty"),
193+
new DummyIdCredentials("garfield-cat", CredentialsScope.GLOBAL, "garfield", "manchu", "Garfield")
194+
));
195+
provider.setDomainCredentialsMap(creds);
196+
197+
// merge creds
198+
Map<Domain, List<Credentials>> update = new HashMap<>();
199+
update.put(null, Arrays.asList(
200+
new DummyIdCredentials("foo-manchu", CredentialsScope.GLOBAL, "foo", "Man-chu", "Dr. Fu Manchu Phd"),
201+
new DummyIdCredentials("strange", CredentialsScope.GLOBAL, "strange", "manchu", "Dr. Strange")
202+
));
203+
Domain catsDotCom2 = new Domain("cats.com", "cats.com domain for cats", Collections.emptyList());
204+
update.put(catsDotCom2, Arrays.asList(
205+
new DummyIdCredentials("garfield-cat", CredentialsScope.GLOBAL, "garfield", "manchu", "Garfield the Cat"),
206+
new DummyIdCredentials("eek-cat", CredentialsScope.GLOBAL, "eek", "manchu", "Eek the Cat")
207+
));
208+
Domain dogsDotCom = new Domain("dogs.com", "dogs.com domain for dogs", Collections.emptyList());
209+
update.put(dogsDotCom, Arrays.asList(
210+
new DummyIdCredentials("snoopy", CredentialsScope.GLOBAL, "snoopy", "manchu", "Snoop-a-Loop")
211+
));
212+
213+
// do merge
214+
provider.mergeDomainCredentialsMap(update);
215+
216+
// verify
217+
List<DomainCredentials> domainCreds = provider.getDomainCredentials();
218+
assertEquals(3, domainCreds.size());
219+
for (DomainCredentials dc : domainCreds) {
220+
if (dc.getDomain().isGlobal()) {
221+
assertEquals(3, dc.getCredentials().size());
222+
assertDummyCreds(dc.getCredentials(), DummyIdCredentials::getUsername, "bar", "foo", "strange");
223+
assertDummyCreds(dc.getCredentials(), DummyIdCredentials::getDescription, "Dr. Bar Manchu", "Dr. Fu Manchu Phd", "Dr. Strange");
224+
} else if (StringUtils.equals(dc.getDomain().getName(), "cats.com")) {
225+
assertEquals("cats.com domain for cats", dc.getDomain().getDescription());
226+
assertEquals(3, dc.getCredentials().size());
227+
assertDummyCreds(dc.getCredentials(), DummyIdCredentials::getUsername, "eek", "garfield", "kitty");
228+
assertDummyCreds(dc.getCredentials(), DummyIdCredentials::getDescription, "Eek the Cat", "Garfield the Cat", "Mrs. Kitty");
229+
} else if (StringUtils.equals(dc.getDomain().getName(), "dogs.com")) {
230+
assertEquals("dogs.com domain for dogs", dc.getDomain().getDescription());
231+
assertEquals(1, dc.getCredentials().size());
232+
assertDummyCreds(dc.getCredentials(), DummyIdCredentials::getUsername, "snoopy");
233+
}
234+
}
235+
}
236+
237+
private <T> void assertDummyCreds(List<Credentials> creds, Function<DummyIdCredentials, T> valSupplier, T... expected) {
238+
List<T> vals = creds.stream()
239+
.filter(c -> c instanceof DummyIdCredentials)
240+
.map(c -> valSupplier.apply((DummyIdCredentials) c))
241+
.sorted()
242+
.collect(Collectors.toList());
243+
assertEquals(Arrays.asList(expected), vals);
244+
}
245+
167246
public static class HasCredentialBuilder extends Builder {
168247

169248
private final String id;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright (c) 2018, CloudBees, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*
24+
*/
25+
26+
package com.cloudbees.plugins.credentials.casc;
27+
28+
import com.cloudbees.plugins.credentials.CredentialsMatchers;
29+
import com.cloudbees.plugins.credentials.CredentialsProvider;
30+
import com.cloudbees.plugins.credentials.CredentialsScope;
31+
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
32+
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
33+
import com.cloudbees.plugins.credentials.domains.Domain;
34+
import com.cloudbees.plugins.credentials.domains.HostnameRequirement;
35+
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl;
36+
import hudson.security.ACL;
37+
import hudson.util.Secret;
38+
import io.jenkins.plugins.casc.ConfigurationAsCode;
39+
import io.jenkins.plugins.casc.ConfigurationContext;
40+
import io.jenkins.plugins.casc.ConfiguratorException;
41+
import io.jenkins.plugins.casc.ConfiguratorRegistry;
42+
import io.jenkins.plugins.casc.misc.ConfiguredWithCode;
43+
import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule;
44+
import io.jenkins.plugins.casc.model.Mapping;
45+
import io.jenkins.plugins.casc.model.Sequence;
46+
import jenkins.model.Jenkins;
47+
import org.junit.After;
48+
import org.junit.Before;
49+
import org.junit.ClassRule;
50+
import org.junit.Test;
51+
import org.jvnet.hudson.test.JenkinsRule;
52+
53+
import java.util.Collections;
54+
import java.util.List;
55+
import java.util.Objects;
56+
import java.util.concurrent.CopyOnWriteArrayList;
57+
58+
import static org.hamcrest.MatcherAssert.assertThat;
59+
import static org.hamcrest.Matchers.equalTo;
60+
import static org.hamcrest.Matchers.hasSize;
61+
import static org.hamcrest.Matchers.is;
62+
import static org.hamcrest.Matchers.not;
63+
import static org.hamcrest.Matchers.nullValue;
64+
import static org.junit.Assert.assertNotNull;
65+
66+
public class MergeSystemCredentialsTest {
67+
68+
@ClassRule
69+
public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule();
70+
71+
@Before
72+
public void setup() {
73+
System.setProperty("casc.credentials.merge.strategy", "merge");
74+
}
75+
76+
@After
77+
public void teardown() {
78+
System.clearProperty("casc.credentials.merge.strategy");
79+
}
80+
81+
@Test
82+
public void merge_system_credentials() throws ConfiguratorException {
83+
UsernamePasswordCredentials foo = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "foo", "", "Foo", "Bar");
84+
UsernamePasswordCredentials bar = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, "bar", "", "Bar", "Foo");
85+
Domain testCom = new Domain("test.com", "test dot com", Collections.emptyList());
86+
SystemCredentialsProvider.getInstance().getCredentials().add(foo);
87+
SystemCredentialsProvider.getInstance().getDomainCredentialsMap().put(testCom, new CopyOnWriteArrayList<>(Collections.singletonList(bar)));
88+
ConfigurationAsCode.get().configure(getClass().getResource("MergeSystemCredentialsTest.yaml").toExternalForm());
89+
System.out.println(SystemCredentialsProvider.getInstance().getDomainCredentialsMap());
90+
List<UsernamePasswordCredentials> ups = CredentialsProvider.lookupCredentials(
91+
UsernamePasswordCredentials.class, j.jenkins, ACL.SYSTEM,
92+
Collections.singletonList(new HostnameRequirement("api.test.com"))
93+
);
94+
assertThat(ups, hasSize(3));
95+
bar = CredentialsMatchers.firstOrNull(ups, CredentialsMatchers.withId("bar"));
96+
assertThat(bar, not(nullValue()));
97+
assertThat(bar.getUsername(), equalTo("bar_usr"));
98+
}
99+
100+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
credentials:
2+
system:
3+
domainCredentials:
4+
- domain:
5+
name: "test.com"
6+
description: "test.com domain"
7+
specifications:
8+
- hostnameSpecification:
9+
includes: "*.test.com"
10+
credentials:
11+
- usernamePassword:
12+
scope: SYSTEM
13+
id: bar
14+
username: bar_usr
15+
password: "pwd"
16+
- usernamePassword:
17+
scope: SYSTEM
18+
id: sudo_password
19+
username: root
20+
password: "password"

0 commit comments

Comments
 (0)