Skip to content

Commit

Permalink
credentials support
Browse files Browse the repository at this point in the history
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Dec 8, 2017
1 parent f156c99 commit 04036d3
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ Here is a list of plugin we have successfuly tested to support configuration-as-
- [x] git plugin ([details](demos/git/README.md))
- [x] artifactory plugin ([details](demos/artifactory/README.md))
- [x] tfs plugin with some limitations ([details](demos/tfs/README.md))
- [x] credentials plugin ([details](demos/credentials/README.md))
- [ ] more to come soon...
46 changes: 46 additions & 0 deletions demos/credentials/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# credentials plugin

## sample configuration

```yaml
credentials:
system:
? name: "test.com"
description: "test.com domain"
specifications:
- hostnameSpecification:
includes:
- "*.test.com"
: - usernamePassword:
scope: SYSTEM
id: sudo_password
username: root
password: 1234
? # "global"
: - certificate:
scope: SYSTEM
id: ssh_private_key
password: ABCD
keyStoreSource:
fileOnMaster:
keyStoreFile: /docker/secret/id_rsa

```

## implementation note

credentials plugin support relies on a custom adaptor component `CredentialsRootConfigurator`.

CredentialsStore uses as internal data model a `Map<Domain, List<Credentials>>`, so the yaml syntax (`? `) to define a
complex mapping key. In previous sample, a Domain key is configured for `*.test.com` domain.

Associated to this key, a list of credentials is defined based on hetero-describable symbol name (note the extra indent
after `usernamePassword` This guy is a single entry map 'usernamePassword' => map of attributes to build target type).

Credentials symbol name is inferred from implementation class simple name: `UsernamePasswordCredentialsImpl`
descriptor's clazz is `Credentials`
we consider the `Impl` suffix as a common pattern to flag implementation class.
=> symbol name is `usernamePassword`


13 changes: 12 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@
<version>1.18</version>
</dependency>

<!-- plugin adapters-->

<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
<version>2.1.16</version>
<optional>true</optional>
</dependency>


<!-- tested plugins -->

<dependency>
Expand Down Expand Up @@ -91,8 +101,9 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>artifactory</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jenkinsci/plugins/casc/Configurator.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static Configurator lookup(Type type) {
return new ExtensionConfigurator(clazz);
}

if (Stapler.CONVERT_UTILS.lookup(clazz) != null) {
if (Stapler.lookupConverter(clazz) != null) {
return new PrimitiveConfigurator(clazz);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ private Class findDescribableBySymbol(String shortname, List<Descriptor> candida
}
}

// Search for implicit symbol, i.e "Foo" for FooCredentialsImpl implementing Credentials
s = shortname + target.getSimpleName() + "Impl";
for (Descriptor d : candidates) {
final String cn = d.getKlass().toJavaClass().getSimpleName();
if (s.equalsIgnoreCase(cn)) {
return d.getKlass().toJavaClass();
}
}

throw new IllegalArgumentException("No "+target.getName()+ "implementation found for "+shortname);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public Object configure(Object config) throws Exception {
if (config == null) throw new IllegalStateException("Environment variable not set: "+var);
}
}
return Stapler.CONVERT_UTILS.convert(config, target);
return Stapler.lookupConverter(target).convert(target, config);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.jenkinsci.plugins.casc.credentials;

import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.domains.Domain;
import hudson.Extension;
import org.jenkinsci.plugins.casc.Attribute;
import org.jenkinsci.plugins.casc.Configurator;
import org.jenkinsci.plugins.casc.RootElementConfigurator;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;


/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
@Extension(optional = true)
public class CredentialsRootConfigurator extends Configurator<CredentialsStore> implements RootElementConfigurator {

private final static Logger logger = Logger.getLogger(CredentialsRootConfigurator.class.getName());


@Override
public String getName() {
return "credentials";
}

@Override
public Class<CredentialsStore> getTarget() {
return CredentialsStore.class;
}

@Override
public CredentialsStore configure(Object config) throws Exception {
Map map = (Map) config;
final Map<?,?> system = (Map) map.get("system");
final Map<Domain, List<Credentials>> target = SystemCredentialsProvider.getInstance().getDomainCredentialsMap();
target.clear();

This comment has been minimized.

Copy link
@jtnord

jtnord Mar 15, 2018

Member

@ndeloof doesn't this get called whenever the config-as-code changes?
if so this will cause intermittent build failures as credentials will go AWOL. whilst the credentials are re-populated?

This comment has been minimized.

Copy link
@ndeloof

ndeloof Mar 16, 2018

Author Contributor

indeed, please log an issue for this. Maybe we will need some changes in credentials plugin.


final Configurator<Domain> domainConfigurator = Configurator.lookup(Domain.class);
final Configurator<Credentials> credentialsConfigurator = Configurator.lookup(Credentials.class);

for (Map.Entry dc : system.entrySet()) {
final Domain domain = domainConfigurator.configure(dc.getKey());
List values = (List) dc.getValue();
final List<Credentials> credentials = new ArrayList<>();
for (Object value : values) {
credentials.add(credentialsConfigurator.configure(value));
}
logger.info("Setting "+target.getClass().getCanonicalName()+"#system["+domain.toString()+"] = " + credentials);
target.put(domain, credentials);
}
return null;
}

@Override
public Set<Attribute> describe() {
return Collections.singleton(new Attribute("system", new HashMap<Domain, List<Credentials>>().getClass()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.jenkinsci.plugins.casc;

import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.cloudbees.plugins.credentials.common.CertificateCredentials;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import hudson.plugins.git.GitTool;
import hudson.security.ACL;
import jenkins.model.Jenkins;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.assertEquals;

/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class SystemCredentialsTest {


@Rule
public JenkinsRule j = new JenkinsRule();

@Test
public void configure_system_credentials() throws Exception {
ConfigurationAsCode.configure(getClass().getResourceAsStream("SystemCredentialsTest.yml"));

List<UsernamePasswordCredentials> ups = CredentialsProvider.lookupCredentials(UsernamePasswordCredentials.class, j.jenkins, ACL.SYSTEM, Collections.EMPTY_LIST);
assertEquals(1, ups.size());
final UsernamePasswordCredentials up = ups.get(0);
assertEquals("1234", up.getPassword().getPlainText());

List<CertificateCredentials> certs = CredentialsProvider.lookupCredentials(CertificateCredentials.class, j.jenkins, ACL.SYSTEM, Collections.EMPTY_LIST);
assertEquals(1, certs.size());
final CertificateCredentials cert = certs.get(0);
assertEquals("ABCD", cert.getPassword().getPlainText());

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
credentials:
system:
? name: "test.com"
description: "test.com domain"
specifications:
- hostnameSpecification:
includes:
- "*.test.com"
: - usernamePassword:
scope: SYSTEM
id: sudo_password
username: root
password: 1234
? # "global"
: - certificate:
scope: SYSTEM
id: ssh_private_key
password: ABCD
keyStoreSource:
fileOnMaster:
keyStoreFile: /docker/secret/id_rsa

0 comments on commit 04036d3

Please sign in to comment.