From 12c5f87ba7782e4d7d2496c5f3fa3c20c0edc7d8 Mon Sep 17 00:00:00 2001 From: Iain Adams Date: Sat, 12 Nov 2022 01:11:55 +0000 Subject: [PATCH] draft for #22 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 --- .../oidc_provider/IdTokenCredentials.java | 18 +++- .../oidc_provider/config/ClaimTemplate.java | 20 ++++ .../config/ClaimTemplate/config.jelly | 5 +- .../ClaimTemplate/help-requiredEnvVars.html | 7 ++ .../config/IdTokenConfiguration/config.jelly | 6 +- .../ConfigurationAsCodeTest.java | 4 +- .../oidc_provider/IdTokenCredentialsTest.java | 93 +++++++++++++++++++ .../jenkins/plugins/oidc_provider/global.yaml | 3 + 8 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/help-requiredEnvVars.html diff --git a/src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java b/src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java index 1de0a65..ce33c32 100644 --- a/src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java +++ b/src/main/java/io/jenkins/plugins/oidc_provider/IdTokenCredentials.java @@ -57,6 +57,7 @@ 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; @@ -64,6 +65,7 @@ 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; @@ -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; /** @@ -201,6 +205,7 @@ RSAPublicKey publicKey() { env = Collections.singletonMap("JENKINS_URL", Jenkins.get().getRootUrl()); } AtomicBoolean definedSub = new AtomicBoolean(); + Map definedClaims = new HashMap<>(); Consumer> addClaims = claimTemplates -> { for (ClaimTemplate t : claimTemplates) { if (STANDARD_CLAIMS.contains(t.name)) { @@ -208,13 +213,24 @@ RSAPublicKey publicKey() { } 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()) { diff --git a/src/main/java/io/jenkins/plugins/oidc_provider/config/ClaimTemplate.java b/src/main/java/io/jenkins/plugins/oidc_provider/config/ClaimTemplate.java index 17a25fd..04ee537 100644 --- a/src/main/java/io/jenkins/plugins/oidc_provider/config/ClaimTemplate.java +++ b/src/main/java/io/jenkins/plugins/oidc_provider/config/ClaimTemplate.java @@ -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; @@ -37,6 +38,7 @@ 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 { @@ -44,6 +46,7 @@ public final class ClaimTemplate extends AbstractDescribableImpl 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; @@ -51,6 +54,15 @@ public final class ClaimTemplate extends AbstractDescribableImpl 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); @@ -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(); + } + } + } } diff --git a/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/config.jelly b/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/config.jelly index 4a02c37..544e0a0 100644 --- a/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/config.jelly +++ b/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/config.jelly @@ -29,9 +29,12 @@ THE SOFTWARE. - + + + + diff --git a/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/help-requiredEnvVars.html b/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/help-requiredEnvVars.html new file mode 100644 index 0000000..ffd3441 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/oidc_provider/config/ClaimTemplate/help-requiredEnvVars.html @@ -0,0 +1,7 @@ +
+ 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. +
+ For example if you wanted a specific claim template for pull requests you can use the CHANGE_ID environment variable which will only be set in multibranch pull requests. +
+ All claim templates are checked but only the first one matching for a given claim name will be used. +
diff --git a/src/main/resources/io/jenkins/plugins/oidc_provider/config/IdTokenConfiguration/config.jelly b/src/main/resources/io/jenkins/plugins/oidc_provider/config/IdTokenConfiguration/config.jelly index 1a905ef..a442671 100644 --- a/src/main/resources/io/jenkins/plugins/oidc_provider/config/IdTokenConfiguration/config.jelly +++ b/src/main/resources/io/jenkins/plugins/oidc_provider/config/IdTokenConfiguration/config.jelly @@ -32,7 +32,7 @@ THE SOFTWARE. - +
@@ -41,7 +41,7 @@ THE SOFTWARE. - +
@@ -50,7 +50,7 @@ THE SOFTWARE. - +
diff --git a/src/test/java/io/jenkins/plugins/oidc_provider/ConfigurationAsCodeTest.java b/src/test/java/io/jenkins/plugins/oidc_provider/ConfigurationAsCodeTest.java index 24504a1..a59fad7 100644 --- a/src/test/java/io/jenkins/plugins/oidc_provider/ConfigurationAsCodeTest.java +++ b/src/test/java/io/jenkins/plugins/oidc_provider/ConfigurationAsCodeTest.java @@ -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())); } diff --git a/src/test/java/io/jenkins/plugins/oidc_provider/IdTokenCredentialsTest.java b/src/test/java/io/jenkins/plugins/oidc_provider/IdTokenCredentialsTest.java index 090c313..783fe2f 100644 --- a/src/test/java/io/jenkins/plugins/oidc_provider/IdTokenCredentialsTest.java +++ b/src/test/java/io/jenkins/plugins/oidc_provider/IdTokenCredentialsTest.java @@ -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)); diff --git a/src/test/resources/io/jenkins/plugins/oidc_provider/global.yaml b/src/test/resources/io/jenkins/plugins/oidc_provider/global.yaml index 861cc0b..5d25abe 100644 --- a/src/test/resources/io/jenkins/plugins/oidc_provider/global.yaml +++ b/src/test/resources/io/jenkins/plugins/oidc_provider/global.yaml @@ -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: \ No newline at end of file