Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: "Tests"

on:
push:
branches:
- "**"
pull_request:

jobs:
java-test:
name: Java Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- workflow files ---'
git ls-files '.github/workflows/*'

printf '%s\n' '--- test.yml with line numbers ---'
cat -n .github/workflows/test.yml

printf '%s\n' '--- workflow permission and checkout settings ---'
rg -n -C 3 '(^|[[:space:]])permissions:|contents:|actions/checkout|persist-credentials|mvn|maven' .github/workflows

Repository: akeylesslabs/akeyless-java-cloud-id

Length of output: 2706


🌐 Web query:

actions/checkout v4 persist-credentials default true README GitHub token permissions contents read workflow

💡 Result:

In the actions/checkout action (including v4), the persist-credentials input defaults to true [1][2][3]. When persist-credentials is set to true, the action configures the provided token or SSH key in the local git configuration [1][4][5]. This allows subsequent git commands run in your workflow steps to use these credentials for authenticated operations [1][6]. The action automatically removes these credentials during the post-job cleanup [1][4][5]. Regarding permissions: 1. Basic checkout: For the actions/checkout action to successfully clone the repository, the GITHUB_TOKEN requires read access to the repository's contents [7]. In workflows where you explicitly define permissions, you must set permissions: contents: read [7]. If you do not specify this, the default permissions for the GITHUB_TOKEN may be set to none (depending on your organization/repository settings), which will cause the checkout to fail [7]. 2. Pushing changes: If you intend to use the persisted credentials to perform git push operations, the GITHUB_TOKEN must have write access to the repository's contents [6]. In this case, you must set permissions: contents: write in your workflow configuration [6]. If persist-credentials is set to false, the action will not configure the token or SSH key in the git config, and subsequent git commands will not be authenticated using those credentials [6].

Citations:


Disable checkout credential persistence

Because Maven runs repository-controlled code, set persist-credentials: false on actions/checkout@v4. Add top-level permissions: contents: read.

🧰 Tools
🪛 zizmor (1.29.0)

[warning] 15-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 15 - 16, Update the workflow’s
actions/checkout@v4 step to set persist-credentials to false, and add top-level
permissions granting contents read access. Keep the existing checkout behavior
unchanged otherwise.

Source: Linters/SAST tools


- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: "11"
distribution: "temurin"
cache: "maven"

- name: Run tests
run: mvn -B -Drevision=1.0.0-SNAPSHOT test
15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@
<artifactId>netty-handler</artifactId>
<version>4.1.121.Final</version>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<distributionManagement>
Expand All @@ -85,6 +93,13 @@

<build>
<plugins>
<!-- runs the JUnit test suite during the test phase -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>

<!-- any other plugins -->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
Expand Down
236 changes: 236 additions & 0 deletions src/test/java/io/akeyless/cloudid/AwsCloudIdProviderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package io.akeyless.cloudid;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Iterator;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
* Fully hermetic, offline end-to-end tests for {@link AwsCloudIdProvider}.
*
* <p>The provider resolves credentials through the AWS SDK
* {@code DefaultCredentialsProvider} chain. The first link in that chain is the
* {@code SystemPropertyCredentialsProvider}, which reads the {@code aws.accessKeyId},
* {@code aws.secretAccessKey} and {@code aws.sessionToken} system properties with
* <em>no</em> network access. By setting those properties to fake static values we
* exercise the whole "sign an STS GetCallerIdentity request and package it as a
* cloud-id token" flow without any real credentials or any outbound request.
*
* <p>SigV4 signing is a purely local computation, so these tests never contact STS.
*/
public class AwsCloudIdProviderTest {

private static final String FAKE_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE";
private static final String FAKE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
private static final String FAKE_SESSION_TOKEN = "FAKESESSIONTOKEN/akeyless-test//////////wEXAMPLE";

private static final String PROP_ACCESS_KEY = "aws.accessKeyId";
private static final String PROP_SECRET_KEY = "aws.secretAccessKey";
private static final String PROP_SESSION_TOKEN = "aws.sessionToken";

private static final ObjectMapper MAPPER = new ObjectMapper();

private String savedAccessKey;
private String savedSecretKey;
private String savedSessionToken;

@Before
public void injectFakeCredentials() {
// Remember whatever the host/CI environment had so we can restore it.
savedAccessKey = System.getProperty(PROP_ACCESS_KEY);
savedSecretKey = System.getProperty(PROP_SECRET_KEY);
savedSessionToken = System.getProperty(PROP_SESSION_TOKEN);

System.setProperty(PROP_ACCESS_KEY, FAKE_ACCESS_KEY);
System.setProperty(PROP_SECRET_KEY, FAKE_SECRET_KEY);
// Session token is set per-test where needed; make sure we start clean.
System.clearProperty(PROP_SESSION_TOKEN);
}

@After
public void restoreCredentials() {
restore(PROP_ACCESS_KEY, savedAccessKey);
restore(PROP_SECRET_KEY, savedSecretKey);
restore(PROP_SESSION_TOKEN, savedSessionToken);
}

private static void restore(String key, String value) {
if (value == null) {
System.clearProperty(key);
} else {
System.setProperty(key, value);
}
}

// ---- The decoded token model -------------------------------------------------

/** Decodes the outer base64+JSON envelope and the inner base64 fields. */
private static DecodedToken decode(String cloudId) throws Exception {
assertNotNull("cloud id must not be null", cloudId);
byte[] outer = Base64.getDecoder().decode(cloudId);
JsonNode root = MAPPER.readTree(new String(outer, StandardCharsets.UTF_8));

DecodedToken token = new DecodedToken();
token.method = root.get("sts_request_method").asText();
token.url = decodeField(root, "sts_request_url");
token.body = decodeField(root, "sts_request_body");
token.headersJson = decodeField(root, "sts_request_headers");
token.headers = MAPPER.readTree(token.headersJson);
return token;
}

private static String decodeField(JsonNode root, String field) {
JsonNode node = root.get(field);
assertNotNull("missing token field: " + field, node);
return new String(Base64.getDecoder().decode(node.asText()), StandardCharsets.UTF_8);
}

private static final class DecodedToken {
String method;
String url;
String body;
String headersJson;
JsonNode headers;

/** Case-insensitive header lookup; headers are serialized as name -> [values]. */
String header(String name) {
Iterator<Map.Entry<String, JsonNode>> fields = headers.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> e = fields.next();
if (e.getKey().equalsIgnoreCase(name)) {
JsonNode v = e.getValue();
return v.isArray() && v.size() > 0 ? v.get(0).asText() : v.asText();
}
}
return null;
}

boolean hasHeader(String name) {
return header(name) != null;
}
}

private static DecodedToken getDecodedToken() throws Exception {
return decode(new AwsCloudIdProvider().getCloudId());
}

// ---- Envelope / payload shape ------------------------------------------------

@Test
public void tokenIsBase64EncodedJsonBundleWithAllFields() throws Exception {
String cloudId = new AwsCloudIdProvider().getCloudId();
assertFalse("cloud id must not be empty", cloudId.isEmpty());

// Outer layer must be valid base64 of a JSON object carrying the 4 STS fields.
JsonNode root = MAPPER.readTree(new String(Base64.getDecoder().decode(cloudId), StandardCharsets.UTF_8));
assertTrue(root.isObject());
assertTrue(root.has("sts_request_method"));
assertTrue(root.has("sts_request_url"));
assertTrue(root.has("sts_request_body"));
assertTrue(root.has("sts_request_headers"));
}

@Test
public void methodIsPost() throws Exception {
assertEquals("POST", getDecodedToken().method);
}

@Test
public void urlDecodesToGlobalStsEndpoint() throws Exception {
assertEquals("https://sts.amazonaws.com/", getDecodedToken().url);
}

@Test
public void bodyIsGetCallerIdentityAction() throws Exception {
assertEquals("Action=GetCallerIdentity&Version=2011-06-15", getDecodedToken().body);
}

// ---- SigV4 signing correctness ----------------------------------------------

@Test
public void authorizationHeaderIsSigV4() throws Exception {
String auth = getDecodedToken().header("Authorization");
assertNotNull("Authorization header must be present", auth);
assertTrue("Authorization must use SigV4 (AWS4-HMAC-SHA256), was: " + auth,
auth.startsWith("AWS4-HMAC-SHA256"));
assertTrue("Authorization must carry a Signature component", auth.contains("Signature="));
assertTrue("Authorization must carry SignedHeaders", auth.contains("SignedHeaders="));
}

@Test
public void credentialScopeTargetsUsEast1StsService() throws Exception {
String auth = getDecodedToken().header("Authorization");
assertNotNull(auth);
// Credential scope is <access-key>/<yyyymmdd>/<region>/<service>/aws4_request
assertTrue("credential scope must target us-east-1/sts, was: " + auth,
auth.contains("/us-east-1/sts/aws4_request"));
assertTrue("Authorization must reference the fake access key id",
auth.contains("Credential=" + FAKE_ACCESS_KEY + "/"));
}

@Test
public void hostHeaderIsGlobalStsHost() throws Exception {
assertEquals("sts.amazonaws.com", getDecodedToken().header("Host"));
}

@Test
public void xAmzDateHeaderIsPresentAndWellFormed() throws Exception {
String date = getDecodedToken().header("X-Amz-Date");
assertNotNull("X-Amz-Date header must be present", date);
// Format is basic ISO8601: yyyyMMdd'T'HHmmss'Z'
assertTrue("X-Amz-Date must match yyyyMMddTHHmmssZ, was: " + date,
date.matches("^\\d{8}T\\d{6}Z$"));
}

@Test
public void contentTypeHeaderIsFormUrlEncoded() throws Exception {
String contentType = getDecodedToken().header("Content-Type");
assertNotNull(contentType);
assertTrue("Content-Type must be form-urlencoded, was: " + contentType,
contentType.startsWith("application/x-www-form-urlencoded"));
}

// ---- Session-token handling --------------------------------------------------

@Test
public void includesSecurityTokenWhenSessionCredentialsUsed() throws Exception {
System.setProperty(PROP_SESSION_TOKEN, FAKE_SESSION_TOKEN);
DecodedToken token = getDecodedToken();
assertEquals("X-Amz-Security-Token must echo the session token",
FAKE_SESSION_TOKEN, token.header("X-Amz-Security-Token"));
// Session-token requests must also fold the token into the signature.
assertTrue(token.header("Authorization").contains("x-amz-security-token"));
}

@Test
public void omitsSecurityTokenForStaticCredentials() throws Exception {
// injectFakeCredentials() clears the session token, so basic creds are used.
DecodedToken token = getDecodedToken();
assertFalse("X-Amz-Security-Token must be absent without a session token",
token.hasHeader("X-Amz-Security-Token"));
}

// ---- Determinism / independence ---------------------------------------------

@Test
public void structureIsDeterministicAcrossCalls() throws Exception {
DecodedToken a = getDecodedToken();
DecodedToken b = getDecodedToken();
// Method/URL/body are fixed inputs and must never vary between invocations.
assertEquals(a.method, b.method);
assertEquals(a.url, b.url);
assertEquals(a.body, b.body);
assertEquals(a.header("Host"), b.header("Host"));
}
}
41 changes: 41 additions & 0 deletions src/test/java/io/akeyless/cloudid/AzureCloudIdProviderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.akeyless.cloudid;

import com.azure.identity.DefaultAzureCredential;
import com.azure.identity.DefaultAzureCredentialBuilder;
import org.junit.Test;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
* Offline tests for {@link AzureCloudIdProvider}.
*
* <p>The provider obtains a token from {@code DefaultAzureCredential}, which requires
* live Azure AD / IMDS access. Per the task constraints we do NOT drive a real token
* fetch here; we only assert what is provable offline: that the provider is
* constructible, is wired into the factory, and that the underlying Azure credential
* builder used by {@code getCloudId()} constructs without any network call.
*/
public class AzureCloudIdProviderTest {

@Test
public void providerIsConstructibleAndImplementsInterface() {
AzureCloudIdProvider provider = new AzureCloudIdProvider();
assertNotNull(provider);
assertTrue(provider instanceof CloudIdProvider);
}

@Test
public void factoryReturnsAzureProvider() {
CloudIdProvider provider = CloudProviderFactory.getCloudIdProvider("azure_ad");
assertTrue(provider instanceof AzureCloudIdProvider);
}

@Test
public void defaultAzureCredentialBuilderConstructsWithoutNetwork() {
// Mirrors the credential construction performed inside getCloudId(). Building
// the credential is a local operation; no token request is issued here.
DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();
assertNotNull("DefaultAzureCredential should build offline", credential);
}
}
Loading
Loading