diff --git a/src/main/java/land/oras/ContainerRef.java b/src/main/java/land/oras/ContainerRef.java index 062c1fcf..65e21f13 100644 --- a/src/main/java/land/oras/ContainerRef.java +++ b/src/main/java/land/oras/ContainerRef.java @@ -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; @@ -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; } diff --git a/src/main/java/land/oras/Registry.java b/src/main/java/land/oras/Registry.java index 5bfcc30f..ddf7f1ec 100644 --- a/src/main/java/land/oras/Registry.java +++ b/src/main/java/land/oras/Registry.java @@ -2050,17 +2050,6 @@ public Builder withReferrerListMaxPages(int referrerListMaxPages) { /** * Set the containers trust policy to enforce during pull operations. - * - *

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. - * - *

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() @@ -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 diff --git a/src/main/java/land/oras/policy/ContainersPolicy.java b/src/main/java/land/oras/policy/ContainersPolicy.java index 8c17dda8..967ff1cf 100644 --- a/src/main/java/land/oras/policy/ContainersPolicy.java +++ b/src/main/java/land/oras/policy/ContainersPolicy.java @@ -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)); } /** @@ -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. @@ -295,11 +274,6 @@ private boolean wildcardMatches(String scope, String pattern) { } private static List 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")); } diff --git a/src/test/java/land/oras/JFrogArtifactoryITCase.java b/src/test/java/land/oras/JFrogArtifactoryITCase.java index 9cf63f9e..df1ab00f 100644 --- a/src/test/java/land/oras/JFrogArtifactoryITCase.java +++ b/src/test/java/land/oras/JFrogArtifactoryITCase.java @@ -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; @@ -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(); diff --git a/src/test/java/land/oras/RegistryTest.java b/src/test/java/land/oras/RegistryTest.java index af102f12..f63bbdf3 100644 --- a/src/test/java/land/oras/RegistryTest.java +++ b/src/test/java/land/oras/RegistryTest.java @@ -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 = @@ -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()); @@ -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 = @@ -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; + } } diff --git a/src/test/java/land/oras/policy/ContainersPolicyTest.java b/src/test/java/land/oras/policy/ContainersPolicyTest.java index 8c1fb36e..3a32376f 100644 --- a/src/test/java/land/oras/policy/ContainersPolicyTest.java +++ b/src/test/java/land/oras/policy/ContainersPolicyTest.java @@ -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"); @@ -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 {