Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SamuelBoerlin committed Sep 17, 2024
0 parents commit 28170a7
Show file tree
Hide file tree
Showing 18 changed files with 1,352 additions and 0 deletions.
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
target

# mvn hpi:run
work

# IntelliJ IDEA project files
*.iml
*.iws
*.ipr
.idea

# Eclipse project files
.settings
.classpath
.project
113 changes: 113 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Jenkins Folder Credentials Importer

This Jenkins plugin allows importing credentials into a folder from an upstream source.
For example, it can be used to import `SYSTEM` scoped (i.e. inaccessible to jobs) credentials from a specific domain in the system store into a domain in the folder store with `GLOBAL` scope so that they are accessible only to jobs within that folder. Like so e.g. all credentials can be configured in a [JCasC](https://www.jenkins.io/projects/jcasc/) config and then be imported into folders.

### Usage

This plugin can only be configured as a folder property with the [Job DSL](https://plugins.jenkins.io/job-dsl/) (or similar) plugin.

For Job DSL:

```
folder {
properties {
importCredentials {
// Whether all other credentials that weren't imported should be removed.
// Default: false.
clear(<bool>)
// Whether credentials should always be re-imported when this property is saved, even if config did not change.
// Default: true.
update(<bool>)
imports {
// Block can be specified multiple times
from {
source {
// One or more ANT glob patterns for matching credentials by id.
// Required.
ids([<pattern>...])
// One or more ANT glob patterns for matching credentials by domain.
// Optional.
domains([<pattern>...])
// One or more URIs for matching credentials by their domain's specifications.
// Note that if a domain does not have any specifications its credentials will always match, regardless of the URIs specified here.
// Optional.
uris([<uri>...])
// One or more strings for matching credentials by scope.
// Valid values: "SYSTEM", "GLOBAL", "USER".
// Default: "GLOBAL", "USER".
scopes([<scope>...])
// One or more strings for matching credentials by source store.
// Valid values: "SYSTEM" (= system store only), "JOB" (= all stores available to job doing the configuration).
// Default: "SYSTEM", "JOB".
sources([<source>...])
// Whether the import should fail when no credentials were matched.
required(<bool>)
}
// Optional block
to {
// If specified the imported credentials' scope is set to this value.
// Optional.
scope(<scope>)
// One or more ANT glob patterns specifying which domains of the imported credentials should be copied into the folder store.
// Optional.
copiedDomains([<pattern>...])
// Domain into which the credentials should be imported if copiedDomains was not specified or did not take effect. The domain is created if it does not exist yet.
// Some other domain specification properties are left out here and can be looked up in the Job DSL domain specification documentation.
// Optional.
defaultDomain {
name(<string>)
}
}
}
}
}
}
}
```

Using this property requires Job DSL jobs to be running with an authentication other than `SYSTEM` (see e.g. [Authorize Project](https://plugins.jenkins.io/authorize-project/)).

Note that the required permissions depend on the configuration. For example, importing credentials from the `SYSTEM` scope requires the `ADMINISTER` permission, and when using `copiedDomains` or `defaultDomain` the `MANAGE_DOMAINS` permission is required. In any case the credentials `CREATE` and `UPDATE` permissions are also required.

### Example

This example imports `SYSTEM` scoped credentials under the 'Source' domain from the system store into the folder store under the 'Target' domain with `GLOBAL` scope.

```
importCredentials {
clear(true)
imports {
from {
source {
ids(['*'])
domains(['Source'])
sources(['SYSTEM'])
scopes(['SYSTEM'])
}
to {
scope('GLOBAL')
defaultDomain {
name('Target')
}
}
}
}
}
```

### Development

Starting a development Jenkins instance with this plugin: `mvn hpi:run`

Building the plugin: `mvn package`
95 changes: 95 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>4.41</version>
<relativePath />
</parent>

<groupId>swiss.dasch.plugins</groupId>
<artifactId>folder-credentials-importer</artifactId>
<version>${revision}</version>
<name>Folder Credentials Importer Plugin</name>

<packaging>hpi</packaging>

<properties>
<revision>1.0</revision>

<jenkins.version>2.332.4</jenkins.version>

<java.level>8</java.level>
</properties>

<repositories>
<repository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>repo.jenkins-ci.org</id>
<url>https://repo.jenkins-ci.org/public/</url>
</pluginRepository>
</pluginRepositories>

<dependencyManagement>
<dependencies>
<dependency>
<!-- Pick up common dependencies for the selected LTS line: https://github.com/jenkinsci/bom#usage -->
<groupId>io.jenkins.tools.bom</groupId>
<artifactId>bom-2.332.x</artifactId>
<version>1451.v15f1fdb_772a_f</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>structs</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>cloudbees-folder</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials</artifactId>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>job-dsl</artifactId>
<version>1.79</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>authorize-project</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>matrix-auth</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>credentials-binding</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jenkins</groupId>
<artifactId>configuration-as-code</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package swiss.dasch.plugins.foldercredentialsimporter;

import java.util.Optional;
import java.util.stream.StreamSupport;

import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import com.cloudbees.hudson.plugins.folder.AbstractFolder;
import com.cloudbees.hudson.plugins.folder.AbstractFolderProperty;
import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsStore;

import hudson.Extension;
import hudson.model.Executor;
import hudson.model.Queue.Executable;
import hudson.security.ACL;
import jenkins.model.Jenkins;

public class CredentialsImporterFolderProperty extends AbstractFolderProperty<AbstractFolder<?>> {

private final Importer importer;

private boolean initialized;

@DataBoundConstructor
public CredentialsImporterFolderProperty(Import[] imports) {
Executor executor = Executor.currentExecutor();
Executable executable = executor != null ? executor.getCurrentExecutable() : null;

// If there's no executable/job and current authentication is already SYSTEM
// then we import the credentials as SYSTEM (e.g. when property is created by
// JCasC). Otherwise when this is created by a job(-dsl) run then we require an
// authentication that is used for importing the credentials other than SYSTEM.
@SuppressWarnings("deprecation")
boolean isSystemImporter = executable == null && Jenkins.getAuthentication() == ACL.SYSTEM;

this.importer = new Importer(imports, isSystemImporter);
}

@DataBoundSetter
public void setClear(boolean clear) {
this.importer.setClear(clear);
}

@DataBoundSetter
public void setUpdate(boolean update) {
this.importer.setUpdate(update);
}

@Override
protected synchronized void setOwner(AbstractFolder<?> owner) {
super.setOwner(owner);

// Only extract credentials once when property is first added/updated
// and not when the property is (re-)loaded from disk
if (this.initialized) {
return;
}

this.initialized = true;

this.importer.fill(owner);
}

public static void importCredentials(AbstractFolder<?> folder) {
CredentialsImporterFolderProperty property = folder.getProperties()
.get(CredentialsImporterFolderProperty.class);

if (property != null) {
Optional<CredentialsStore> storeLookup = StreamSupport
.stream(CredentialsProvider.lookupStores(folder).spliterator(), false)
.filter(s -> s.getContext() == folder).findFirst();

storeLookup.ifPresent(property.importer::into);
}
}

@Extension
@Symbol("importCredentials")
public static class DescriptorImpl extends AbstractFolderPropertyDescriptor {
@DataBoundConstructor
public DescriptorImpl() {
}

@Override
public String getDisplayName() {
return Messages.FolderCredentialsImporter_DisplayName();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package swiss.dasch.plugins.foldercredentialsimporter;

public enum CredentialsSource {
/**
* Credentials from
* {@link com.cloudbees.plugins.credentials.SystemCredentialsProvider}
*/
SYSTEM,

/**
* Credentials available to job (includes {@link CredentialsSource#SYSTEM}
* credentials)
*/
JOB
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package swiss.dasch.plugins.foldercredentialsimporter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainCredentials;

public class DomainCredentialsBuilder {

private final Map<Domain, List<Credentials>> credentials = new LinkedHashMap<>();

public void addCredentials(Domain domain, List<Credentials> credentials) {
this.credentials.computeIfAbsent(domain, d -> new ArrayList<>()).addAll(credentials);
}

public void addCredentials(Domain domain, Credentials credentials) {
this.addCredentials(domain, Collections.singletonList(credentials));
}

public void addCredentials(DomainCredentials credentials) {
this.addCredentials(credentials.getDomain(), credentials.getCredentials());
}

public void addCredentials(List<DomainCredentials> credentials) {
for (DomainCredentials dc : credentials) {
this.addCredentials(dc);
}
}

public List<DomainCredentials> build() {
List<DomainCredentials> credentials = new ArrayList<>();

for (Entry<Domain, List<Credentials>> entry : this.credentials.entrySet()) {
credentials.add(new DomainCredentials(entry.getKey(), entry.getValue()));
}

return credentials;
}

}
Loading

0 comments on commit 28170a7

Please sign in to comment.