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
19 changes: 15 additions & 4 deletions src/main/java/land/oras/ContainerRef.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import land.oras.exception.OrasException;
import land.oras.policy.ContainersPolicy;
import land.oras.policy.Transport;
import land.oras.utils.Const;
import land.oras.utils.SupportedAlgorithm;
Expand Down Expand Up @@ -560,15 +561,25 @@ public boolean isBlocked(Registry registry) {
effectiveRef);
return true;
}
return !isAllowed(effectiveRef, registry.getContainersPolicy());
}

/**
* Check if an effective (already resolved) reference is blocked by the given policy
* @param effectiveRef The effective reference
* @param policy The policy
* @return True or false
*/
public boolean isAllowed(ContainerRef effectiveRef, ContainersPolicy policy) {
// Check containers policy. Strip a trailing ":tag" and/or "@digest" without touching a
// "host:port" registry (the tag colon always follows the last "/").
String scope = effectiveRef.toString().replaceFirst("(:[^/@]+)?(@[^/]+)?$", "");
boolean allowed = registry.getContainersPolicy().isAllowed(Transport.DOCKER, scope);
if (!allowed) {
throw new OrasException("Image '%s' rejected by containers policy".formatted(this));
boolean allowed = policy.isAllowed(Transport.DOCKER, scope);
if (allowed) {
LOG.debug("Access to container reference {} is allowed by policy", effectiveRef);
return true;
}

LOG.info("Access to container reference {} is not allowed by policy", effectiveRef);
return false;
}

Expand Down
24 changes: 0 additions & 24 deletions src/main/java/land/oras/Registry.java
Original file line number Diff line number Diff line change
Expand Up @@ -2050,17 +2050,6 @@ public Builder withReferrerListMaxPages(int referrerListMaxPages) {

/**
* Set the containers trust policy to enforce during pull operations.
*
* <p>When set, all image manifest pulls will be evaluated against the policy before
* being returned. The {@code insecureAcceptAnything}, {@code reject}, and
* {@code sigstoreSigned} (keyed verification of attached Sigstore bundles) requirements are
* enforced. The {@code signedBy} (GPG simple signing) requirement is not implemented and will
* log a warning and accept the image without verification.
*
* <p>By default, the policy is loaded from standard locations
* ({@code $HOME/.config/containers/policy.json} or {@code /etc/containers/policy.json}).
* If no policy file is found, an accept-all policy is used.
*
* @param policy the containers policy to enforce.
* @return the builder
* @see ContainersPolicy#newPolicy()
Expand All @@ -2070,19 +2059,6 @@ public Builder withPolicy(ContainersPolicy policy) {
return this;
}

/**
* Load and set the containers trust policy from the given path.
*
* @param policyPath the path to the policy.json file.
* @return the builder
* @throws OrasException if the file cannot be read or parsed.
* @see ContainersPolicy#newPolicy(Path)
*/
public Builder withPolicy(Path policyPath) {
registry.setContainersPolicy(ContainersPolicy.newPolicy(policyPath));
return this;
}

/**
* Return a new builder
* @return The builder
Expand Down
28 changes: 1 addition & 27 deletions src/main/java/land/oras/policy/ContainersPolicy.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static ContainersPolicy newPolicy() {
}

LOG.warn("No containers policy.json found; using insecureAcceptAnything default");
return acceptAll();
return new ContainersPolicy(PolicyFile.fromJson(List.of(new PolicyRequirement.InsecureAcceptAnything()), null));
}

/**
Expand All @@ -112,27 +112,6 @@ public static ContainersPolicy newPolicy(Path path) {
}
}

/**
* Create a policy that accepts any image unconditionally.
*
* @return a permissive {@link ContainersPolicy}.
*/
public static ContainersPolicy acceptAll() {
PolicyFile policyFile =
new PolicyFile(List.of(new PolicyRequirement.InsecureAcceptAnything()), Collections.emptyMap());
return new ContainersPolicy(policyFile);
}

/**
* Create a policy that rejects every image unconditionally.
*
* @return a rejecting {@link ContainersPolicy}.
*/
public static ContainersPolicy rejectAll() {
PolicyFile policyFile = new PolicyFile(List.of(new PolicyRequirement.Reject()), Collections.emptyMap());
return new ContainersPolicy(policyFile);
}

/**
* Determine whether an image is allowed under this policy using the lightweight, content-free
* scope gate.
Expand Down Expand Up @@ -295,11 +274,6 @@ private boolean wildcardMatches(String scope, String pattern) {
}

private static List<Path> defaultPolicyPaths() {
String home = System.getenv("HOME");
if (home != null) {
return List.of(
Path.of(home, ".config", "containers", "policy.json"), Path.of("/etc/containers/policy.json"));
}
return List.of(Path.of("/etc/containers/policy.json"));
}

Expand Down
13 changes: 13 additions & 0 deletions src/test/java/land/oras/JFrogArtifactoryITCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Path;
import land.oras.policy.ContainersPolicy;
import land.oras.utils.ZotUnsecureContainer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand All @@ -42,6 +43,18 @@ class JFrogArtifactoryITCase {
@Container
private final ZotUnsecureContainer unsecureRegistry = new ZotUnsecureContainer().withStartupAttempts(3);

@Test
void shouldPullWithSignature() {
Registry registry = Registry.builder()
.defaults()
.withPolicy(ContainersPolicy.newPolicy())
.build();
ContainerRef containerRef1 =
ContainerRef.parse("prj-elca-ai-oci.artifactory.svc.elca.ch/skills/jenkins-pipeline:0.0.17");
Manifest manifest = registry.getManifest(containerRef1);
assertNotNull(manifest);
}

@Test
void shouldPull() {
Registry registry = Registry.builder().build();
Expand Down
47 changes: 39 additions & 8 deletions src/test/java/land/oras/RegistryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2439,19 +2439,24 @@ void shouldPushArtifactWithChunkedOptionsAndAnnotations() throws IOException {
}

@Test
void shouldBlockPullWithRejectPolicy() throws IOException {
void shouldBlockPullWithRejectPolicy(@TempDir Path policyDir) throws IOException {

// Create 2 policies
ContainersPolicy acceptALl = ContainersPolicy.newPolicy(createAcceptAllPolicy(policyDir));
ContainersPolicy rejectAll = ContainersPolicy.newPolicy(createRejectPolicy(policyDir));

// Create a registry with reject-all policy
Registry registryWithPolicy = Registry.Builder.builder()
.defaults("myuser", "mypass")
.withInsecure(true)
.withPolicy(ContainersPolicy.rejectAll())
.withPolicy(rejectAll)
.build();

// First push an artifact without policy enforcement
Registry registryNormal = Registry.Builder.builder()
.defaults("myuser", "mypass")
.withInsecure(true)
.withPolicy(ContainersPolicy.acceptAll())
.withPolicy(acceptALl)
.build();

ContainerRef containerRef =
Expand All @@ -2467,8 +2472,8 @@ void shouldBlockPullWithRejectPolicy() throws IOException {
// Pull fails with reject-all policy - verify exact error message
OrasException ex = assertThrows(OrasException.class, () -> registryWithPolicy.getManifest(containerRef));
assertTrue(
ex.getMessage().contains("rejected by containers policy"),
"Expected error message to contain 'rejected by containers policy', got: " + ex.getMessage());
ex.getMessage().contains("is blocked by registry configuration"),
"Expected error message to contain 'is blocked by registry configuration', got: " + ex.getMessage());
assertTrue(
ex.getMessage().contains(containerRef.toString()),
"Expected error message to contain container ref, got: " + ex.getMessage());
Expand All @@ -2486,14 +2491,13 @@ void shouldBlockPullWithPolicyFromPath() throws IOException {
Registry registryWithPolicy = Registry.Builder.builder()
.defaults("myuser", "mypass")
.withInsecure(true)
.withPolicy(policyFile)
.withPolicy(ContainersPolicy.newPolicy(policyFile))
.build();

// Push an artifact first (with accept-all policy)
Registry registryNormal = Registry.Builder.builder()
.defaults("myuser", "mypass")
.withInsecure(true)
.withPolicy(ContainersPolicy.acceptAll())
.build();

ContainerRef containerRef =
Expand All @@ -2508,9 +2512,36 @@ void shouldBlockPullWithPolicyFromPath() throws IOException {

// Pull fails with policy from file - verify exact error message format
OrasException ex = assertThrows(OrasException.class, () -> registryWithPolicy.getManifest(containerRef));
String expectedMessage = "Image '%s' rejected by containers policy".formatted(containerRef);
String expectedMessage =
"Access to container reference %s is blocked by registry configuration".formatted(containerRef);
assertEquals(expectedMessage, ex.getMessage());

Files.deleteIfExists(policyFile);
}

private Path createRejectPolicy(Path tempDir) throws IOException {
Path output = Path.of(tempDir.toString(), "reject.json");
Files.writeString(
output,
// language=json
"""
{
"default": [{"type": "reject"}]
}
""");
return output;
}

private Path createAcceptAllPolicy(Path tempDir) throws IOException {
Path output = Path.of(tempDir.toString(), "accept.json");
Files.writeString(
output,
// language=json
"""
{
"default": [{"type": "insecureAcceptAnything"}]
}
""");
return output;
}
}
32 changes: 0 additions & 32 deletions src/test/java/land/oras/policy/ContainersPolicyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,6 @@
@Execution(ExecutionMode.SAME_THREAD)
class ContainersPolicyTest {

@Test
void acceptAllPolicyAllowsEverything() {
ContainersPolicy policy = ContainersPolicy.acceptAll();
assertTrue(policy.isAllowed(Transport.DOCKER, "docker.io/library/nginx"));
assertTrue(policy.isAllowed(Transport.DOCKER, "quay.io/foo/bar"));
assertTrue(policy.isAllowed(Transport.UNKNOWN, ""));
}

@Test
void rejectAllPolicyDeniesEverything() {
ContainersPolicy policy = ContainersPolicy.rejectAll();
assertFalse(policy.isAllowed(Transport.DOCKER, "docker.io/library/nginx"));
assertFalse(policy.isAllowed(Transport.DOCKER, "quay.io/foo/bar"));
}

@Test
void loadAcceptAllFromFile() {
Path path = resourcePath("policy/accept-all.json");
Expand Down Expand Up @@ -191,23 +176,6 @@ void loadsUserPolicyFromHome(@TempDir Path homeDir) throws Exception {
});
}

@Test
void userPolicyTakesPrecedenceOverSystemPolicy(@TempDir Path homeDir) throws Exception {
// language=json
String userPolicyJson = """
{"default": [{"type": "reject"}]}
""";
writePolicyFile(homeDir, userPolicyJson);

TestUtils.withHome(homeDir, () -> {
ContainersPolicy policy = ContainersPolicy.newPolicy();
assertNotNull(policy);
// User's reject-all should win (we can't know what system policy says, but we know
// the user policy was loaded since docker.io is rejected)
assertFalse(policy.isAllowed(Transport.DOCKER, "docker.io/library/nginx"));
});
}

@Test
@Execution(ExecutionMode.SAME_THREAD)
void fallsBackToAcceptAllWhenNoPolicyFileFound(@TempDir Path emptyHome) throws Exception {
Expand Down
Loading