Skip to content

Commit

Permalink
draft for jenkinsci#22
Browse files Browse the repository at this point in the history
fix feedback :+1

switch @DataBoundConstructor back to original constructor

Update src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java

Co-authored-by: Francisco Javier Fernandez <31063239+fcojfernandez@users.noreply.github.com>

fix missing import

revert StringUtils.isNotBlank change

pr feedback

fix jelly template
  • Loading branch information
iwarapter committed Nov 22, 2022
1 parent 182a02f commit 12c5f87
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
Expand All @@ -74,6 +76,8 @@

public abstract class IdTokenCredentials extends BaseStandardCredentials {

private static final Logger LOGGER = Logger.getLogger(Keys.class.getName());

private static final long serialVersionUID = 1;

/**
Expand Down Expand Up @@ -201,20 +205,32 @@ RSAPublicKey publicKey() {
env = Collections.singletonMap("JENKINS_URL", Jenkins.get().getRootUrl());
}
AtomicBoolean definedSub = new AtomicBoolean();
Map<String, Boolean> definedClaims = new HashMap<>();
Consumer<List<ClaimTemplate>> addClaims = claimTemplates -> {
for (ClaimTemplate t : claimTemplates) {
if (STANDARD_CLAIMS.contains(t.name)) {
throw new SecurityException("An id token claim template must not specify " + t.name);
} else if (t.name.equals(Claims.SUBJECT)) {
definedSub.set(true);
}
builder.claim(t.name, t.type.parse(Util.replaceMacro(t.format, env)));
if (t.getRequiredEnvVars() == null || env.keySet().containsAll(Arrays.asList(t.getRequiredEnvVars().split("\\s*\\s")))) {
if (definedClaims.containsKey(t.name)) {
LOGGER.fine(() -> "declining to set claim: " + t.name + " as it has already been set.");
} else {
LOGGER.fine(() -> "setting claim: " + t.name + " with format: " + t.format);
builder.claim(t.name, t.type.parse(Util.replaceMacro(t.format, env)));
definedClaims.put(t.name, true);
}
}
}
};
LOGGER.fine("setting claim templates");
addClaims.accept(cfg.getClaimTemplates());
if (build != null) {
LOGGER.fine("setting build claim templates");
addClaims.accept(cfg.getBuildClaimTemplates());
} else {
LOGGER.fine("setting global claim templates");
addClaims.accept(cfg.getGlobalClaimTemplates());
}
if (!definedSub.get()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

package io.jenkins.plugins.oidc_provider.config;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.Util;
Expand All @@ -37,20 +38,31 @@
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;

public final class ClaimTemplate extends AbstractDescribableImpl<ClaimTemplate> {

public final @NonNull String name;
public final @NonNull String format;
public final @NonNull ClaimType type;
private @CheckForNull String requiredEnvVars;

@DataBoundConstructor public ClaimTemplate(String name, String format, ClaimType type) {
this.name = name;
this.format = format;
this.type = type;
}

@DataBoundSetter
public void setRequiredEnvVars(String vars) {
this.requiredEnvVars = Util.fixEmpty(vars);
}

public String getRequiredEnvVars() {
return this.requiredEnvVars;
}

@Restricted(NoExternalUse.class)
public String xmlForm() {
return Jenkins.XSTREAM2.toXML(this);
Expand All @@ -77,6 +89,14 @@ public FormValidation doCheckName(@QueryParameter String value) {
}
}

public FormValidation doCheckRequiredEnvVars(@QueryParameter String value) {
if (!value.equals(value.toUpperCase())) {
return FormValidation.warning("Defined environment variables should be in upper case.");
} else {
return FormValidation.ok();
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ THE SOFTWARE.
<f:textbox/>
</f:entry>
<f:entry field="format" title="${%Value format}">
<f:textbox/>
<f:textbox />
</f:entry>
<f:entry field="type" title="${%Value type}">
<f:dropdownDescriptorSelector field="type" default="${descriptor.defaultType}"/>
</f:entry>
<f:entry title="Required Environment Variables" field="requiredEnvVars">
<f:textbox/>
</f:entry>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div>
A space separated list of required environment variables for this claim to take precedence. This can be useful for setting claims with certain attributes for given scenarios.
<br/>
For example if you wanted a specific claim template for pull requests you can use the <code>CHANGE_ID</code> environment variable which will only be set in multibranch pull requests.
<br/>
All claim templates are checked but only the first one matching for a given claim name will be used.
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ THE SOFTWARE.

<f:advanced title="${%Claim templates}" align="left">
<f:entry field="claimTemplates" title="${%General claim templates}">
<f:repeatableProperty field="claimTemplates">
<f:repeatableProperty field="claimTemplates" header="Template">
<f:block>
<div align="right">
<f:repeatableDeleteButton/>
Expand All @@ -41,7 +41,7 @@ THE SOFTWARE.
</f:repeatableProperty>
</f:entry>
<f:entry field="buildClaimTemplates" title="${%Build-scoped claim templates}">
<f:repeatableProperty field="buildClaimTemplates">
<f:repeatableProperty field="buildClaimTemplates" header="Template">
<f:block>
<div align="right">
<f:repeatableDeleteButton/>
Expand All @@ -50,7 +50,7 @@ THE SOFTWARE.
</f:repeatableProperty>
</f:entry>
<f:entry field="globalClaimTemplates" title="${%Globally-scoped claim templates}">
<f:repeatableProperty field="globalClaimTemplates">
<f:repeatableProperty field="globalClaimTemplates" header="Template">
<f:block>
<div align="right">
<f:repeatableDeleteButton/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ public class ConfigurationAsCodeTest {
ClaimTemplate.xmlForm(cfg.getClaimTemplates()));
assertEquals(ClaimTemplate.xmlForm(Collections.singletonList(new ClaimTemplate("sub", "jenkins", new StringClaimType()))),
ClaimTemplate.xmlForm(cfg.getGlobalClaimTemplates()));
assertEquals(ClaimTemplate.xmlForm(Arrays.asList(new ClaimTemplate("sub", "${JOB_NAME}", new StringClaimType()), new ClaimTemplate("num", "${BUILD_NUMBER}", new IntegerClaimType()))),
ClaimTemplate claimWithRequired = new ClaimTemplate("sub", "${JOB_NAME}", new StringClaimType());
claimWithRequired.setRequiredEnvVars("JOB_NAME");
assertEquals(ClaimTemplate.xmlForm(Arrays.asList(claimWithRequired, new ClaimTemplate("num", "${BUILD_NUMBER}", new IntegerClaimType()))),
ClaimTemplate.xmlForm(cfg.getBuildClaimTemplates()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,99 @@ public class IdTokenCredentialsTest {
});
}

@Test public void requiredEnvironmentVariablesAreHonoured() throws Throwable {
rr.then(r -> {
IdTokenStringCredentials c = new IdTokenStringCredentials(CredentialsScope.GLOBAL, "test", null);
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), c);
IdTokenConfiguration cfg = IdTokenConfiguration.get();
cfg.setClaimTemplates(Collections.singletonList(new ClaimTemplate("ok", "true", new BooleanClaimType())));
cfg.setGlobalClaimTemplates(Collections.singletonList(new ClaimTemplate("sub", "jenkins", new StringClaimType())));

ClaimTemplate claimWithRequired1 = new ClaimTemplate("sub", "${JOB_NAME}:custom", new StringClaimType());
claimWithRequired1.setRequiredEnvVars("JOB_NAME BUILD_ID");
ClaimTemplate claimWithRequired2 = new ClaimTemplate("sub", "${JOB_NAME}", new StringClaimType());
claimWithRequired2.setRequiredEnvVars("JOB_NAME");
cfg.setBuildClaimTemplates(Arrays.asList(
claimWithRequired1,
claimWithRequired2,
new ClaimTemplate("num", "${BUILD_NUMBER}", new IntegerClaimType())));


String idToken = c.getSecret().getPlainText();
System.out.println(idToken);
Claims claims = Jwts.parserBuilder().
setSigningKey(c.publicKey()).
build().
parseClaimsJws(idToken).
getBody();
System.out.println(claims);
assertEquals(r.jenkins.getRootUrl() + "oidc", claims.getIssuer());
assertEquals("jenkins", claims.getSubject());
assertTrue(claims.get("ok", Boolean.class));
WorkflowJob p = r.createProject(Folder.class, "dir").createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("withCredentials([string(variable: 'TOK', credentialsId: 'test')]) {env.TOK = TOK}", true));
WorkflowRun b = r.buildAndAssertSuccess(p);
EnvironmentAction env = b.getAction(EnvironmentAction.class);
idToken = env.getEnvironment().get("TOK");
System.out.println(idToken);
claims = Jwts.parserBuilder().
setSigningKey(c.publicKey()).
build().
parseClaimsJws(idToken).
getBody();
System.out.println(claims);
assertEquals(r.jenkins.getRootUrl() + "oidc", claims.getIssuer());
assertEquals("dir/p:custom", claims.getSubject());
assertEquals(1, claims.get("num", Integer.class).intValue());
assertTrue(claims.get("ok", Boolean.class));
});
}

@Test public void ifNoRequiredEnvironmentVariablesAreMatched() throws Throwable {
rr.then(r -> {
IdTokenStringCredentials c = new IdTokenStringCredentials(CredentialsScope.GLOBAL, "test", null);
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), c);
IdTokenConfiguration cfg = IdTokenConfiguration.get();
cfg.setClaimTemplates(Collections.singletonList(new ClaimTemplate("ok", "true", new BooleanClaimType())));
cfg.setGlobalClaimTemplates(Collections.singletonList(new ClaimTemplate("sub", "jenkins", new StringClaimType())));

ClaimTemplate claimWithRequired = new ClaimTemplate("sub", "${JOB_NAME}", new StringClaimType());
claimWithRequired.setRequiredEnvVars("JOB_NAME THIS_WILL_NEVER_BE_SET");
cfg.setBuildClaimTemplates(Arrays.asList(
claimWithRequired,
new ClaimTemplate("sub", "${JOB_NAME}", new StringClaimType()), //fallback sub
new ClaimTemplate("num", "${BUILD_NUMBER}", new IntegerClaimType())));

String idToken = c.getSecret().getPlainText();
System.out.println(idToken);
Claims claims = Jwts.parserBuilder().
setSigningKey(c.publicKey()).
build().
parseClaimsJws(idToken).
getBody();
System.out.println(claims);
assertEquals(r.jenkins.getRootUrl() + "oidc", claims.getIssuer());
assertEquals("jenkins", claims.getSubject());
assertTrue(claims.get("ok", Boolean.class));
WorkflowJob p = r.createProject(Folder.class, "dir").createProject(WorkflowJob.class, "p");
p.setDefinition(new CpsFlowDefinition("withCredentials([string(variable: 'TOK', credentialsId: 'test')]) {env.TOK = TOK}", true));
WorkflowRun b = r.buildAndAssertSuccess(p);
EnvironmentAction env = b.getAction(EnvironmentAction.class);
idToken = env.getEnvironment().get("TOK");
System.out.println(idToken);
claims = Jwts.parserBuilder().
setSigningKey(c.publicKey()).
build().
parseClaimsJws(idToken).
getBody();
System.out.println(claims);
assertEquals(r.jenkins.getRootUrl() + "oidc", claims.getIssuer());
assertEquals("dir/p", claims.getSubject());
assertEquals(1, claims.get("num", Integer.class).intValue());
assertTrue(claims.get("ok", Boolean.class));
});
}

@Test public void invalidCustomClaims() throws Throwable {
rr.then(r -> {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), new IdTokenStringCredentials(CredentialsScope.GLOBAL, "test", null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ security:
- name: sub
format: jenkins
type: string
requiredEnvVars: ""
buildClaimTemplates:
- name: sub
format: ^${JOB_NAME}
type: string
requiredEnvVars: "JOB_NAME"
- name: num
format: ^${BUILD_NUMBER}
type: integer
requiredEnvVars:

0 comments on commit 12c5f87

Please sign in to comment.