diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProp.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProp.java new file mode 100644 index 00000000000..5f18bda8746 --- /dev/null +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProp.java @@ -0,0 +1,93 @@ +package com.yahoo.athenz.instance.provider.impl; + +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.yahoo.athenz.auth.token.jwts.JwtsHelper; +import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; + +import com.nimbusds.jose.proc.SecurityContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class InstanceGithubActionsProp { + + private final Map properties = new HashMap<>(); + + // Inner class to hold property data + private static class Prop { + String audience; + String enterprise; + Set dnsSuffixes; + String jwksUri; + ConfigurableJWTProcessor jwtProcessor; + + Prop(String dnsSuffix, String audience, String enterprise, String jwksUri) { + dnsSuffixes = Set.of(dnsSuffix.split(",")); + this.audience = audience; + this.enterprise = enterprise; + this.jwksUri = jwksUri; + this.jwtProcessor = JwtsHelper.getJWTProcessor(new JwtsSigningKeyResolver(jwksUri, null)); + } + } + + // No-argument constructor + public InstanceGithubActionsProp() { + } + + // Method to add properties + public void addProperties(String issuer, String providerDnsSuffix, String audience, String enterprise, String jwksUri) { + if (issuer == null || providerDnsSuffix == null || audience == null || jwksUri == null) { + throw new IllegalArgumentException("One of the required properties is null"); + } + properties.put(issuer, new Prop(providerDnsSuffix, audience, enterprise, jwksUri)); + } + + public Boolean hasIssuer(String issuer) { + if (issuer == null || issuer.isEmpty()) { + return false; + } + return properties.containsKey(issuer); + } + + // Getter methods + public Set getDnsSuffixes(String issuer) { + if (!properties.containsKey(issuer)) { + return null; + } + return properties.get(issuer).dnsSuffixes; + } + + public String getAudience(String issuer) { + if (!properties.containsKey(issuer)) { + return null; + } + return properties.get(issuer).audience; + } + + public String getEnterprise(String issuer) { + if (!properties.containsKey(issuer)) { + return null; + } + return properties.get(issuer).enterprise; + } + + public Boolean hasEnterprise (String issuer) { + String enterprise = getEnterprise(issuer); + return enterprise != null && !enterprise.isEmpty(); + } + + public String getJwksUri(String issuer) { + if (!properties.containsKey(issuer)) { + return null; + } + return properties.get(issuer).jwksUri; + } + + public ConfigurableJWTProcessor getJwtProcessor(String issuer) { + if (!properties.containsKey(issuer)) { + return null; + } + return properties.get(issuer).jwtProcessor; + } +} diff --git a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java index 75c8a6a4214..eb2a31a5231 100644 --- a/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java +++ b/libs/java/instance_provider/src/main/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProvider.java @@ -15,15 +15,17 @@ */ package com.yahoo.athenz.instance.provider.impl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.type.TypeReference; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; import com.yahoo.athenz.auth.Authorizer; import com.yahoo.athenz.auth.KeyStore; import com.yahoo.athenz.auth.Principal; import com.yahoo.athenz.auth.impl.SimplePrincipal; import com.yahoo.athenz.auth.token.jwts.JwtsHelper; -import com.yahoo.athenz.auth.token.jwts.JwtsSigningKeyResolver; import com.yahoo.athenz.common.server.util.config.dynamic.DynamicConfigLong; import com.yahoo.athenz.instance.provider.InstanceConfirmation; import com.yahoo.athenz.instance.provider.InstanceProvider; @@ -33,8 +35,13 @@ import org.slf4j.LoggerFactory; import javax.net.ssl.SSLContext; + +import java.io.IOException; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.TimeUnit; +import java.nio.file.Files; +import java.nio.file.Path; import static com.yahoo.athenz.common.server.util.config.ConfigManagerSingleton.CONFIG_MANAGER; @@ -45,13 +52,21 @@ public class InstanceGithubActionsProvider implements InstanceProvider { private static final String URI_INSTANCE_ID_PREFIX = "athenz://instanceid/"; private static final String URI_SPIFFE_PREFIX = "spiffe://"; - static final String GITHUB_ACTIONS_PROP_PROVIDER_DNS_SUFFIX = "athenz.zts.github_actions.provider_dns_suffix"; + static final String KEY_PROVIDER_DNS_SUFFIX = "provider_dns_suffix"; + static final String KEY_AUDIENCE = "audience"; + static final String KEY_ENTERPRISE = "enterprise"; + static final String KEY_JWKS_URI = "jwks_uri"; + static final String KEY_ISSUER = "issuer"; + static final String KEY_JWK_PROCESSOR = "jwk_processor"; + + static final String GITHUB_ACTIONS_PROP_FILE_PATH = "athenz.zts.github_actions.prop_file_path"; + static final String GITHUB_ACTIONS_PROP_PROVIDER_DNS_SUFFIX = "athenz.zts.github_actions." + KEY_PROVIDER_DNS_SUFFIX; static final String GITHUB_ACTIONS_PROP_BOOT_TIME_OFFSET = "athenz.zts.github_actions.boot_time_offset"; static final String GITHUB_ACTIONS_PROP_CERT_EXPIRY_TIME = "athenz.zts.github_actions.cert_expiry_time"; - static final String GITHUB_ACTIONS_PROP_ENTERPRISE = "athenz.zts.github_actions.enterprise"; - static final String GITHUB_ACTIONS_PROP_AUDIENCE = "athenz.zts.github_actions.audience"; - static final String GITHUB_ACTIONS_PROP_ISSUER = "athenz.zts.github_actions.issuer"; - static final String GITHUB_ACTIONS_PROP_JWKS_URI = "athenz.zts.github_actions.jwks_uri"; + static final String GITHUB_ACTIONS_PROP_ENTERPRISE = "athenz.zts.github_actions." + KEY_ENTERPRISE; + static final String GITHUB_ACTIONS_PROP_AUDIENCE = "athenz.zts.github_actions." + KEY_AUDIENCE; + static final String GITHUB_ACTIONS_PROP_ISSUER = "athenz.zts.github_actions." + KEY_ISSUER; + static final String GITHUB_ACTIONS_PROP_JWKS_URI = "athenz.zts.github_actions." + KEY_JWKS_URI; static final String GITHUB_ACTIONS_ISSUER = "https://token.actions.githubusercontent.com"; static final String GITHUB_ACTIONS_ISSUER_JWKS_URI = "https://token.actions.githubusercontent.com/.well-known/jwks"; @@ -61,12 +76,8 @@ public class InstanceGithubActionsProvider implements InstanceProvider { public static final String CLAIM_EVENT_NAME = "event_name"; public static final String CLAIM_REPOSITORY = "repository"; - Set dnsSuffixes = null; - String githubIssuer = null; + InstanceGithubActionsProp props = null; String provider = null; - String audience = null; - String enterprise = null; - ConfigurableJWTProcessor jwtProcessor = null; Authorizer authorizer = null; DynamicConfigLong bootTimeOffsetSeconds; long certExpiryTime; @@ -76,49 +87,81 @@ public Scheme getProviderScheme() { return Scheme.CLASS; } + public void initializeFromFilePath() throws ProviderResourceException { + final String propFilePath = System.getProperty(GITHUB_ACTIONS_PROP_FILE_PATH, ""); + if (StringUtil.isEmpty(propFilePath)) { + return; // no prop file path specified, nothing to do + } + + Path path = Paths.get(propFilePath); + try { + Map>> propJson = new ObjectMapper().readValue( + Files.readAllBytes(path), + new TypeReference>>>() { } + ); + + for (Map prop : propJson.get("props")) { + String issuer = (String) prop.get(KEY_ISSUER); + if (StringUtil.isEmpty(issuer)) { + throw forbiddenError("Missing required issuer prop file: " + propFilePath); + } + + props.addProperties( + issuer, + (String) prop.get(KEY_PROVIDER_DNS_SUFFIX), + (String) prop.get(KEY_AUDIENCE), + (String) prop.get(KEY_ENTERPRISE), + extractGitHubIssuerJwksUri(issuer, (String) prop.get(KEY_JWKS_URI)) + ); + } + + } catch (IOException ex) { + throw forbiddenError("Unable to parse jwk endpoints file: " + propFilePath + + ", error: " + ex.getMessage()); + } + } + @Override public void initialize(String provider, String providerEndpoint, SSLContext sslContext, KeyStore keyStore) { + props = new InstanceGithubActionsProp(); + // save our provider name this.provider = provider; - // lookup the zts audience. if not specified we'll default to athenz.io - - audience = System.getProperty(GITHUB_ACTIONS_PROP_AUDIENCE, "athenz.io"); - - // determine the dns suffix. if this is not specified we'll just default to github-actions.athenz.cloud - - final String dnsSuffix = System.getProperty(GITHUB_ACTIONS_PROP_PROVIDER_DNS_SUFFIX, "github-actions.athenz.io"); - dnsSuffixes = new HashSet<>(); - dnsSuffixes.addAll(Arrays.asList(dnsSuffix.split(","))); - // how long the instance must be booted in the past before we // stop validating the instance requests long timeout = TimeUnit.SECONDS.convert(5, TimeUnit.MINUTES); bootTimeOffsetSeconds = new DynamicConfigLong(CONFIG_MANAGER, GITHUB_ACTIONS_PROP_BOOT_TIME_OFFSET, timeout); - // determine if we're running in enterprise mode - - enterprise = System.getProperty(GITHUB_ACTIONS_PROP_ENTERPRISE); - // get default/max expiry time for any generated tokens - 6 hours certExpiryTime = Long.parseLong(System.getProperty(GITHUB_ACTIONS_PROP_CERT_EXPIRY_TIME, "360")); - // initialize our jwt processor + String githubIssuer = System.getProperty(GITHUB_ACTIONS_PROP_ISSUER, GITHUB_ACTIONS_ISSUER); + + props.addProperties( + githubIssuer, + System.getProperty(GITHUB_ACTIONS_PROP_PROVIDER_DNS_SUFFIX, "github-actions.athenz.io"), // determine the dns suffix. if this is not specified we'll just default to github-actions.athenz.cloud + System.getProperty(GITHUB_ACTIONS_PROP_AUDIENCE, "athenz.io"), // lookup the zts audience. if not specified we'll default to athenz.io + System.getProperty(GITHUB_ACTIONS_PROP_ENTERPRISE), // determine if we're running in enterprise mode + extractGitHubIssuerJwksUri(githubIssuer, System.getProperty(GITHUB_ACTIONS_PROP_JWKS_URI)) + ); - githubIssuer = System.getProperty(GITHUB_ACTIONS_PROP_ISSUER, GITHUB_ACTIONS_ISSUER); - jwtProcessor = JwtsHelper.getJWTProcessor(new JwtsSigningKeyResolver(extractGitHubIssuerJwksUri(githubIssuer), null)); + try { + initializeFromFilePath(); // initialize from file path if specified. If not specified, nothing happens. + } catch (ProviderResourceException ex) { + LOGGER.error("Unable to initialize from file path: {}", ex.getMessage()); + } } - String extractGitHubIssuerJwksUri(final String issuer) { + String extractGitHubIssuerJwksUri(final String issuer, String jwksUri) { // if we have the value configured then that's what we're going to use - String jwksUri = System.getProperty(GITHUB_ACTIONS_PROP_JWKS_URI); if (!StringUtil.isEmpty(jwksUri)) { return jwksUri; } @@ -187,17 +230,26 @@ public InstanceConfirmation confirmInstance(InstanceConfirmation confirmation) t } StringBuilder errMsg = new StringBuilder(256); + String claimIssuer = null; + try { + // parse the token and get the issuer claim + claimIssuer = SignedJWT.parse(attestationData).getJWTClaimsSet().getIssuer(); + } catch (Exception ex) { + errMsg.append("Unable to parse token: ").append(ex.getMessage()); + throw forbiddenError("Unable to parse token: " + ex.getMessage()); + } + final String reqInstanceId = InstanceUtils.getInstanceProperty(instanceAttributes, InstanceProvider.ZTS_INSTANCE_ID); - if (!validateOIDCToken(attestationData, instanceDomain, instanceService, reqInstanceId, errMsg)) { - throw forbiddenError("Unable to validate Certificate Request: " + errMsg); + if (!validateOIDCToken(claimIssuer, attestationData, instanceDomain, instanceService, reqInstanceId, errMsg)) { + throw forbiddenError("Unable to validate Certificate Request: " + errMsg); } // validate the certificate san DNS names StringBuilder instanceId = new StringBuilder(256); if (!InstanceUtils.validateCertRequestSanDnsNames(instanceAttributes, instanceDomain, - instanceService, dnsSuffixes, null, null, false, instanceId, null)) { + instanceService, props.getDnsSuffixes(claimIssuer), null, null, false, instanceId, null)) { throw forbiddenError("Unable to validate certificate request sanDNS entries"); } @@ -245,11 +297,16 @@ boolean validateSanUri(final String sanUri) { return true; } - boolean validateOIDCToken(final String jwToken, final String domainName, final String serviceName, + boolean validateOIDCToken(final String claimIssuer, final String jwToken, final String domainName, final String serviceName, final String instanceId, StringBuilder errMsg) { + if (props == null) { + errMsg.append("InstanceGithubActionsProp not initialized"); + return false; + } + ConfigurableJWTProcessor jwtProcessor = props.getJwtProcessor(claimIssuer); if (jwtProcessor == null) { - errMsg.append("JWT Processor not initialized"); + errMsg.append("JWT Processor not found for issuer: ").append(claimIssuer); return false; } @@ -261,25 +318,15 @@ boolean validateOIDCToken(final String jwToken, final String domainName, final S return false; } - // verify the issuer in set to GitHub Actions - - if (!githubIssuer.equals(claimsSet.getIssuer())) { - errMsg.append("token issuer is not GitHub Actions: ").append(claimsSet.getIssuer()); - return false; - } - - // verify that token audience is set for our service - - if (!audience.equals(JwtsHelper.getAudience(claimsSet))) { + if (!props.getAudience(claimIssuer).equals(JwtsHelper.getAudience(claimsSet))) { errMsg.append("token audience is not ZTS Server audience: ").append(JwtsHelper.getAudience(claimsSet)); return false; } // verify that token issuer is set for our enterprise if one is configured - - if (!StringUtil.isEmpty(enterprise)) { + if (props.hasEnterprise(claimIssuer)) { final String tokenEnterprise = JwtsHelper.getStringClaim(claimsSet, CLAIM_ENTERPRISE); - if (!enterprise.equals(tokenEnterprise)) { + if (!props.getEnterprise(claimIssuer).equals(tokenEnterprise)) { errMsg.append("token enterprise is not the configured enterprise: ").append(tokenEnterprise); return false; } diff --git a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsPropTest.java b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsPropTest.java new file mode 100644 index 00000000000..7c49677f74e --- /dev/null +++ b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsPropTest.java @@ -0,0 +1,120 @@ +/* + * Copyright The Athenz Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.yahoo.athenz.instance.provider.impl; + +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +import java.util.Set; + +public class InstanceGithubActionsPropTest { + + private InstanceGithubActionsProp instanceGithubActionsProp; + + @BeforeMethod + public void setUp() { + instanceGithubActionsProp = new InstanceGithubActionsProp(); + } + + @Test + public void testAddPropertiesAndGetters() { + String issuer = "testIssuer"; + String providerDnsSuffix = "testDnsSuffix"; + String audience = "testAudience"; + String enterprise = "testEnterprise"; + String jwksUri = "https://example.com/jwks"; + + instanceGithubActionsProp.addProperties(issuer, providerDnsSuffix, audience, enterprise, jwksUri); + + Set gotDnsSuffixes = instanceGithubActionsProp.getDnsSuffixes(issuer); + assertEquals(gotDnsSuffixes.size(), 1); + assertTrue(gotDnsSuffixes.contains(providerDnsSuffix)); + assertEquals(instanceGithubActionsProp.getAudience(issuer), audience); + assertEquals(instanceGithubActionsProp.getEnterprise(issuer), enterprise); + assertEquals(instanceGithubActionsProp.getJwksUri(issuer), jwksUri); + + ConfigurableJWTProcessor jwtProcessor = instanceGithubActionsProp.getJwtProcessor(issuer); + assertNotNull(jwtProcessor); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddPropertiesWithNullIssuer() { + instanceGithubActionsProp.addProperties(null, "dnsSuffix", "audience", "enterprise", "https://example.com/jwks"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddPropertiesWithNullProviderDnsSuffix() { + instanceGithubActionsProp.addProperties("issuer", null, "audience", "enterprise", "https://example.com/jwks"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddPropertiesWithNullAudience() { + instanceGithubActionsProp.addProperties("issuer", "dnsSuffix", null, "enterprise", "https://example.com/jwks"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddPropertiesWithNullJwksUri() { + instanceGithubActionsProp.addProperties("issuer", "dnsSuffix", "audience", "enterprise", null); + } + + @Test + public void testHasIssuer() { + String issuer = "testIssuer"; + instanceGithubActionsProp.addProperties(issuer, "dnsSuffix", "audience", "enterprise", "https://example.com/jwks"); + + assertTrue(instanceGithubActionsProp.hasIssuer(issuer)); + assertFalse(instanceGithubActionsProp.hasIssuer("nonExistentIssuer")); + } + + @Test + public void testGettersReturnNullForNonExistentIssuer() { + String nonExistentIssuer = "nonExistentIssuer"; + + assertFalse(instanceGithubActionsProp.hasIssuer(nonExistentIssuer)); + assertFalse(instanceGithubActionsProp.hasIssuer("")); + assertFalse(instanceGithubActionsProp.hasIssuer(null)); + assertNull(instanceGithubActionsProp.getDnsSuffixes(nonExistentIssuer)); + assertNull(instanceGithubActionsProp.getAudience(nonExistentIssuer)); + assertNull(instanceGithubActionsProp.getEnterprise(nonExistentIssuer)); + assertNull(instanceGithubActionsProp.getJwksUri(nonExistentIssuer)); + assertNull(instanceGithubActionsProp.getJwtProcessor(nonExistentIssuer)); + } + + @Test + public void testHasEnterprise() { + String issuerWithEnterprise = "issuerWithEnterprise"; + String issuerWithoutEnterprise = "issuerWithoutEnterprise"; + String issuerWithEmptyEnterprise = "issuerWithEmptyEnterprise"; + + instanceGithubActionsProp.addProperties(issuerWithEnterprise, "dnsSuffix", "audience", "enterprise", "https://example.com/jwks"); + instanceGithubActionsProp.addProperties(issuerWithoutEnterprise, "dnsSuffix", "audience", null, "https://example.com/jwks"); + instanceGithubActionsProp.addProperties(issuerWithEmptyEnterprise, "dnsSuffix", "audience", "", "https://example.com/jwks"); + + assertTrue(instanceGithubActionsProp.hasEnterprise(issuerWithEnterprise)); + assertFalse(instanceGithubActionsProp.hasEnterprise(issuerWithoutEnterprise)); + assertFalse(instanceGithubActionsProp.hasEnterprise(issuerWithEmptyEnterprise)); + } + + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddPropertiesWithNullValues() { + instanceGithubActionsProp.addProperties(null, "dnsSuffix", "audience", "enterprise", "https://example.com/jwks"); + } +} diff --git a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java index 14406732471..4040238363e 100644 --- a/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java +++ b/libs/java/instance_provider/src/test/java/com/yahoo/athenz/instance/provider/impl/InstanceGithubActionsProviderTest.java @@ -43,6 +43,7 @@ import java.util.Map; import java.util.HashMap; import java.util.Objects; +import java.util.Set; import static org.testng.Assert.*; @@ -87,6 +88,63 @@ public void testInitializeWithConfig() { assertEquals(provider.getProviderScheme(), InstanceProvider.Scheme.CLASS); } + @Test + public void testInitializeFromFilePathException() { + // Create a new instance of the provider + InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); + + // Set a system property to an invalid file path to trigger an exception + System.setProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_FILE_PATH, "/invalid/path/to/config.json"); + + // Call the initialize method + provider.initialize("sys.auth.github_actions", + "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); + } + + @Test + public void testInitializeFromFilePath() { + // Set the system property to the relative path of the JSON file + String filePath = this.getClass().getClassLoader().getResource("github-action-props.json").getPath(); + System.setProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_FILE_PATH, filePath); + + // Create a new instance of the provider + InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); + + // Call the initialize method + provider.initialize("sys.auth.github_actions", + "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); + + // Verify that properties were loaded correctly + assertNotNull(provider.props); + assertTrue(provider.props.hasIssuer("https://issuer1.com")); + assertTrue(provider.props.hasIssuer("https://issuer2.com")); + + // Optionally verify specific property values + Set gotDnsSuffixes = provider.props.getDnsSuffixes("https://issuer1.com"); + assertEquals(gotDnsSuffixes.size(), 2); + assertTrue(gotDnsSuffixes.contains("example-suffix1")); + assertTrue(gotDnsSuffixes.contains("example-suffix2")); + assertEquals(provider.props.getAudience("https://issuer1.com"), "https://audience1.com"); + } + + @Test + public void testInitializeFromFilePathIgnoresMissingIssuer() { + // Set the system property to the relative path of the JSON file with missing issuer + String filePath = this.getClass().getClassLoader().getResource("github-action-props-missing-issuer.json").getPath(); + System.setProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_FILE_PATH, filePath); + + // Create a new instance of the provider + InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); + + // Call the initialize method + provider.initialize("sys.auth.github_actions", + "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); + + // Verify that no properties were added for the missing issuer + assertFalse(provider.props.hasIssuer(""), "Entry with missing issuer should be ignored"); + } + + @Test public void testInitializeWithOpenIdConfig() throws IOException { @@ -356,6 +414,38 @@ public void testConfirmInstanceWithoutAttestationData() { } } + // TODO: + @Test + public void testConfirmInstanceTokenParsingException() { + final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); + System.setProperty(InstanceGithubActionsProvider.GITHUB_ACTIONS_PROP_JWKS_URI, jwksUri); + InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); + provider.initialize("sys.auth.github_actions", + "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); + + Authorizer authorizer = Mockito.mock(Authorizer.class); + provider.setAuthorizer(authorizer); + + InstanceConfirmation confirmation = new InstanceConfirmation(); + confirmation.setDomain("sports"); + confirmation.setService("api"); + confirmation.setProvider("sys.auth.github-actions"); + + // Define an invalid JWT + String invalidJwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid.payload.signature"; + confirmation.setAttestationData(invalidJwt); + + try { + provider.confirmInstance(confirmation); + fail("Expected ProviderResourceException due to token parsing failure"); + } catch (ProviderResourceException ex) { + // Verify that the exception message is correct + assertEquals(ex.getCode(), 403); + assertTrue(ex.getMessage().contains("Unable to parse token: ")); + } + } + + @Test public void testRefreshNotSupported() { final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); @@ -386,17 +476,29 @@ public void testValidateSanUri() { } @Test - public void testValidateOIDCTokenWithoutJWTProcessor() { + public void testValidateOIDCTokenWithoutInstanceGithubActionsProp() { InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); - + String issuer = "https://token.actions.githubusercontent.com"; StringBuilder errMsg = new StringBuilder(256); - assertFalse(provider.validateOIDCToken("some-jwt", "sports", "api", "athenz:sia:0001", errMsg)); - assertTrue(errMsg.toString().contains("JWT Processor not initialized")); + assertFalse(provider.validateOIDCToken(issuer, "some-jwt", "sports", "api", "athenz:sia:0001", errMsg)); + assertTrue(errMsg.toString().contains("InstanceGithubActionsProp not initialized")); provider.close(); } + // @Test + // public void testValidateOIDCTokenWithoutJWTProcessor() { + + // InstanceGithubActionsProvider provider = new InstanceGithubActionsProvider(); + // String issuer = "https://token.actions.githubusercontent.com"; + // StringBuilder errMsg = new StringBuilder(256); + // assertFalse(provider.validateOIDCToken(issuer, "some-jwt", "sports", "api", "athenz:sia:0001", errMsg)); + // assertTrue(errMsg.toString().contains("JWT Processor not initialized")); + + // provider.close(); + // } + @Test public void testValidateOIDCTokenIssuerMismatch() throws JOSEException { final String jwksUri = Objects.requireNonNull(classLoader.getResource("jwt_jwks.json")).toString(); @@ -409,12 +511,13 @@ public void testValidateOIDCTokenIssuerMismatch() throws JOSEException { // our issuer will not match - String idToken = generateIdToken("https://token-actions.githubusercontent.com", + String wrongIssuer = "https://token-actions.githubusercontent.com"; + String idToken = generateIdToken(wrongIssuer, System.currentTimeMillis() / 1000, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(wrongIssuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); - assertTrue(errMsg.toString().contains("token issuer is not GitHub Actions")); + assertTrue(errMsg.toString().contains("JWT Processor not found for issuer: ")); } @Test @@ -429,10 +532,11 @@ public void testValidateOIDCTokenAudienceMismatch() throws JOSEException { // our audience will not match - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token audience is not ZTS Server audience")); } @@ -450,10 +554,11 @@ public void testValidateOIDCTokenEnterpriseMismatch() throws JOSEException { // our enterprise will not match - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token enterprise is not the configured enterprise")); } @@ -471,19 +576,19 @@ public void testValidateOIDCTokenStartNotRecentEnough() throws JOSEException { // our issue time is not recent enough - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000 - 400, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("job start time is not recent enough")); // create another token without the issue time - - idToken = generateIdToken("https://token.actions.githubusercontent.com", + idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, true, false, false); errMsg.setLength(0); - result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("job start time is not recent enough")); } @@ -500,25 +605,25 @@ public void testValidateOIDCTokenRunIdMismatch() throws JOSEException { "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); // our issue time is not recent enough - - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:1001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("invalid instance id: athenz:sia:0001/athenz:sia:1001")); - idToken = generateIdToken("https://token.actions.githubusercontent.com", + idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, true, false); errMsg.setLength(0); - result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:1001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token does not contain required run_id or repository claims")); - idToken = generateIdToken("https://token.actions.githubusercontent.com", + idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, false, true); errMsg.setLength(0); - result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:1001", errMsg); + result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:1001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token does not contain required run_id or repository claims")); } @@ -538,12 +643,12 @@ public void testValidateOIDCTokenMissingEventName() throws JOSEException { //provider.signingKeyResolver.addPublicKey("0", Crypto.loadPublicKey(ecPublicKey)); // create an id token without the event_name claim - - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, true, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token does not contain required event_name claim")); } @@ -560,12 +665,12 @@ public void testValidateOIDCTokenMissingSubject() throws JOSEException { "class://com.yahoo.athenz.instance.provider.impl.InstanceGithubActionsProvider", null, null); // create an id token without the subject claim - - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, true, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("token does not contain required subject claim")); } @@ -589,12 +694,12 @@ public void testValidateOIDCTokenAuthorizationFailure() throws JOSEException { provider.setAuthorizer(authorizer); // create an id token - - String idToken = generateIdToken("https://token.actions.githubusercontent.com", + String issuer = "https://token.actions.githubusercontent.com"; + String idToken = generateIdToken(issuer, System.currentTimeMillis() / 1000, false, false, false, false, false); StringBuilder errMsg = new StringBuilder(256); - boolean result = provider.validateOIDCToken(idToken, "sports", "api", "athenz:sia:0001", errMsg); + boolean result = provider.validateOIDCToken(issuer, idToken, "sports", "api", "athenz:sia:0001", errMsg); assertFalse(result); assertTrue(errMsg.toString().contains("authorization check failed for action")); } diff --git a/libs/java/instance_provider/src/test/resources/github-action-props-missing-issuer.json b/libs/java/instance_provider/src/test/resources/github-action-props-missing-issuer.json new file mode 100644 index 00000000000..d4ac512bbda --- /dev/null +++ b/libs/java/instance_provider/src/test/resources/github-action-props-missing-issuer.json @@ -0,0 +1,10 @@ +{ + "props": [ + { + "provider_dns_suffix": "example-suffix1,example-suffix2", + "enterprise": "enterprise1", + "audience": "https://audience1.com", + "jwks_uri": "https://issuer1.com/jwks" + } + ] +} diff --git a/libs/java/instance_provider/src/test/resources/github-action-props.json b/libs/java/instance_provider/src/test/resources/github-action-props.json new file mode 100644 index 00000000000..0e3f7490f14 --- /dev/null +++ b/libs/java/instance_provider/src/test/resources/github-action-props.json @@ -0,0 +1,18 @@ +{ + "props": [ + { + "provider_dns_suffix": "example-suffix1,example-suffix2", + "enterprise": "enterprise1", + "audience": "https://audience1.com", + "issuer": "https://issuer1.com", + "jwks_uri": "https://issuer1.com/jwks" + }, + { + "provider_dns_suffix": "example-suffix3,example-suffix4", + "enterprise": "enterprise2", + "audience": "https://audience2.com", + "issuer": "https://issuer2.com", + "jwks_uri": "https://issuer2.com/jwks" + } + ] +} \ No newline at end of file