From 3e9c956bee957433562db23642649ecf8f0c7fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4ckle?= Date: Wed, 22 Apr 2026 13:25:44 +0200 Subject: [PATCH] add ITs for policy entry references (eclipse-ditto/ditto#2423) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace entriesAdditions and importsAliases integration tests with new policy entry references tests, covering import references (inheriting resources from imported entries), local references (inheriting subjects from local entries), combined references, and referential integrity validation (409 on delete conflicts, 400 on broken references). New test classes: - PolicyEntryReferencesIT: CRUD + referential integrity (18 tests) - PolicyEntryImportReferencesIT: import reference behavior (19 tests) - PolicyEntryLocalReferencesIT: local reference behavior (9 tests) - PolicyEntryCombinedReferencesIT: import + local combined (7 tests) Adapted existing tests: - PolicyImportTransitiveImportsIT: use import references in setup - PolicyEntryImportableSubResourcesIT: use import references in setup - SearchWithTransitiveImportsIT: use import references in setup Deleted 8 obsolete test files for entriesAdditions/importsAliases. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Thomas Jäckle --- .../testing/common/ResourcePathBuilder.java | 6 + ...rchWithPolicyImportEntriesAdditionsIT.java | 541 --------- .../SearchWithTransitiveImportsIT.java | 77 +- .../QueryThingsWithImportsAliasesIT.java | 234 ---- .../rest/PolicyEntryCombinedReferencesIT.java | 524 +++++++++ .../rest/PolicyEntryImportReferencesIT.java | 910 +++++++++++++++ .../PolicyEntryImportableSubResourcesIT.java | 211 ++-- .../rest/PolicyEntryLocalReferencesIT.java | 506 ++++++++ .../things/rest/PolicyEntryReferencesIT.java | 626 ++++++++++ .../rest/PolicyImportEntriesAdditionsIT.java | 433 ------- .../rest/PolicyImportSubResourcesIT.java | 481 -------- .../rest/PolicyImportTransitiveImportsIT.java | 172 +-- .../things/rest/PolicyImportsAliasesIT.java | 610 ---------- ...ithImportedPoliciesEntriesAdditionsIT.java | 1022 ----------------- .../ws/PolicyImportEntriesAdditionsWsIT.java | 252 ---- .../ws/PolicyImportsAliasesWebSocketIT.java | 350 ------ 16 files changed, 2868 insertions(+), 4087 deletions(-) delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithPolicyImportEntriesAdditionsIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/search/things/QueryThingsWithImportsAliasesIT.java create mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryCombinedReferencesIT.java create mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportReferencesIT.java create mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryLocalReferencesIT.java create mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryReferencesIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportEntriesAdditionsIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportSubResourcesIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportsAliasesIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/rest/ThingsWithImportedPoliciesEntriesAdditionsIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportEntriesAdditionsWsIT.java delete mode 100644 system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportsAliasesWebSocketIT.java diff --git a/common/src/main/java/org/eclipse/ditto/testing/common/ResourcePathBuilder.java b/common/src/main/java/org/eclipse/ditto/testing/common/ResourcePathBuilder.java index 1a4e1e3..97e444a 100644 --- a/common/src/main/java/org/eclipse/ditto/testing/common/ResourcePathBuilder.java +++ b/common/src/main/java/org/eclipse/ditto/testing/common/ResourcePathBuilder.java @@ -221,6 +221,12 @@ public Policy policyEntries() { return this; } + public Policy policyEntryReferences(final CharSequence label) { + policyEntry(label); + stringBuilder.append(SLASH).append("references"); + return this; + } + public Policy policyImports() { stringBuilder.append(SLASH).append("imports"); return this; diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithPolicyImportEntriesAdditionsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithPolicyImportEntriesAdditionsIT.java deleted file mode 100644 index 2adcc90..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithPolicyImportEntriesAdditionsIT.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.search.security; - -import static org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.testing.common.matcher.search.SearchProperties.attribute; -import static org.eclipse.ditto.testing.common.matcher.search.SearchProperties.featureProperty; -import static org.eclipse.ditto.testing.common.matcher.search.SearchResponseMatchers.isCount; -import static org.eclipse.ditto.testing.common.matcher.search.SearchResponseMatchers.isEmpty; -import static org.eclipse.ditto.testing.common.matcher.search.SearchResponseMatchers.isEqualTo; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; -import static org.eclipse.ditto.thingsearch.model.SearchModelFactory.and; - -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionFactory; -import org.eclipse.ditto.base.model.acks.DittoAcknowledgementLabel; -import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyEntry; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Resource; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.testing.common.SearchIntegrationTest; -import org.eclipse.ditto.testing.common.TestConstants; -import org.eclipse.ditto.things.model.ThingId; -import org.junit.Before; -import org.junit.Test; - -/** - * Search integration tests verifying that the search index correctly applies authorization based on - * policy import {@code entriesAdditions}. Uses {@code SEARCH_PERSISTED} acknowledgment for initial - * consistency and awaitility-based polling for assertions after policy mutations. - */ -public final class SearchWithPolicyImportEntriesAdditionsIT extends SearchIntegrationTest { - - private static final ConditionFactory AWAITILITY_SEARCH_CONFIG = - Awaitility.await().atMost(30, TimeUnit.SECONDS).pollInterval(3, TimeUnit.SECONDS); - - private PolicyId importedPolicyId; - private PolicyId importingPolicyId; - private Subject defaultSubject; - private Subject subject2; - - @Before - public void setUp() { - importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - } - - @Test - public void secondUserFindsThingViaSubjectAddition() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with SEARCH_PERSISTED ack - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(thingId, importingPolicyId); - - // user2 finds the thing via search - searchThings(V_2) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Also verify count - searchCount(V_2) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isCount(1)) - .fire(); - } - - @Test - public void secondUserDoesNotFindThingAfterSubjectAdditionRemoved() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with SEARCH_PERSISTED ack - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(thingId, importingPolicyId); - - // Verify user2 finds the thing initially - searchThings(V_2) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Remove entriesAdditions (update import without additions) - final PolicyImport importWithoutAdditions = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - putPolicyImport(importingPolicyId, importWithoutAdditions) - .expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT) - .fire(); - - // user2 no longer finds the thing - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void secondUserDoesNotFindThingAfterTemplateEntryDeleted() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with SEARCH_PERSISTED ack - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(thingId, importingPolicyId); - - // Verify user2 finds the thing initially - searchThings(V_2) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Delete DEFAULT entry from template - deletePolicyEntry(importedPolicyId, "DEFAULT") - .expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT) - .fire(); - - // user2 no longer finds the thing - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void secondUserDoesNotFindThingAfterTemplateSetToNotImportable() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with SEARCH_PERSISTED ack - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(thingId, importingPolicyId); - - // Verify user2 finds the thing initially - searchThings(V_2) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Update template DEFAULT to importable=NEVER - final PolicyEntry neverDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.NEVER, Set.of(AllowedImportAddition.SUBJECTS)); - putPolicyEntry(importedPolicyId, neverDefaultEntry) - .expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT) - .fire(); - - // user2 no longer finds the thing - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void subjectAdditionWithFineGrainedTemplateGrantAllowsAttributeSearchOnly() { - // Template grants thing:/attributes READ only (not thing:/), allows subject additions - final Policy importedPolicy = buildImportedPolicyWithResources(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS), - List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())))); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 via subject addition - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with attributes and features - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(buildThingJson(thingId, importingPolicyId)); - - // user2 finds the thing by attribute (template grants thing:/attributes READ) - searchThings(V_2) - .filter(and(idFilter(thingId), attribute("manufacturer").eq("ACME"))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // user2 does NOT find the thing by feature property (no READ on thing:/features) - searchThings(V_2) - .filter(and(idFilter(thingId), featureProperty("sensor", "temperature").eq(42))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void resourceAdditionGrantsFeaturePropertySearchAccess() { - // Template grants thing:/attributes READ, allows subject + resource additions - final Policy importedPolicy = buildImportedPolicyWithResources(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES), - List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())))); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy adds user2 (subject) + thing:/features READ (resource addition) - final Resource featuresResource = PoliciesModelFactory.newResource(thingResource("/features"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final Policy importingPolicy = buildImportingPolicyWithSubjectAndResourceAdditions( - importingPolicyId, importedPolicyId, subject2, featuresResource); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with attributes and features - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(buildThingJson(thingId, importingPolicyId)); - - // user2 finds the thing by attribute (template grant) - searchThings(V_2) - .filter(and(idFilter(thingId), attribute("manufacturer").eq("ACME"))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // user2 finds the thing by feature property (resource addition) - searchThings(V_2) - .filter(and(idFilter(thingId), featureProperty("sensor", "temperature").eq(42))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Remove entriesAdditions (update import without additions) - final PolicyImport importWithoutAdditions = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - putPolicyImport(importingPolicyId, importWithoutAdditions) - .expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT) - .fire(); - - // user2 no longer finds the thing by feature property (resource addition removed) - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(and(idFilter(thingId), featureProperty("sensor", "temperature").eq(42))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - - // user2 no longer finds the thing by attribute either (subject addition also removed) - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(and(idFilter(thingId), attribute("manufacturer").eq("ACME"))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void resourceAdditionOnSpecificFeatureAllowsSearchOnlyForThatFeature() { - // Template grants thing:/attributes READ, allows subject + resource additions - final Policy importedPolicy = buildImportedPolicyWithResources(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES), - List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())))); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Importing policy: subject addition (user2) + resource addition only for sensor properties - final Resource sensorPropsResource = PoliciesModelFactory.newResource( - thingResource("/features/sensor/properties"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final Policy importingPolicy = buildImportingPolicyWithSubjectAndResourceAdditions( - importingPolicyId, importedPolicyId, subject2, sensorPropsResource); - putPolicy(importingPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create thing with attributes, sensor and actuator features - final ThingId thingId = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(buildThingJson(thingId, importingPolicyId)); - - // user2 finds the thing by attribute (template grant) - searchThings(V_2) - .filter(and(idFilter(thingId), attribute("manufacturer").eq("ACME"))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // user2 finds the thing by sensor feature property (resource addition on sensor) - searchThings(V_2) - .filter(and(idFilter(thingId), featureProperty("sensor", "temperature").eq(42))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // user2 does NOT find the thing by actuator feature property (no READ on actuator) - searchThings(V_2) - .filter(and(idFilter(thingId), featureProperty("actuator", "active").eq(true))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void multipleThingsFromMultipleImportersAllFoundThenAllLost() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Two importing policies, each adds user2 via entriesAdditions - final PolicyId importingPolicyId2 = PolicyId.of(idGenerator().withPrefixedRandomName("importing2")); - - final Policy importingPolicy1 = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy1).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - final Policy importingPolicy2 = buildImportingPolicyWithSubjectAdditions( - importingPolicyId2, importedPolicyId, subject2); - putPolicy(importingPolicy2).expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.CREATED).fire(); - - // Create two things (one per importing policy) with SEARCH_PERSISTED ack - final ThingId thingId1 = ThingId.of(importingPolicyId); - putThingAndWaitForSearchIndex(thingId1, importingPolicyId); - - final ThingId thingId2 = ThingId.of(importingPolicyId2); - putThingAndWaitForSearchIndex(thingId2, importingPolicyId2); - - // user2 finds both things - searchThings(V_2) - .filter(idsFilter(List.of(thingId1, thingId2))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEqualTo(toThingResult(thingId1, thingId2))) - .fire(); - - // Modify template: change DEFAULT to importable=NEVER - final PolicyEntry neverDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.NEVER, Set.of(AllowedImportAddition.SUBJECTS)); - putPolicyEntry(importedPolicyId, neverDefaultEntry) - .expectingHttpStatus(org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT) - .fire(); - - // user2 no longer finds either thing - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idsFilter(List.of(thingId1, thingId2))) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingBody(isEmpty()) - .fire(); - } - - private void putThingAndWaitForSearchIndex(final ThingId thingId, final PolicyId policyId) { - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId.toString()) - .set("policyId", policyId.toString()) - .build(), - V_2) - .withHeader(DittoHeaderDefinition.REQUESTED_ACKS.getKey(), - DittoAcknowledgementLabel.SEARCH_PERSISTED.toString()) - .withHeader(DittoHeaderDefinition.TIMEOUT.getKey(), "30s") - .expectingStatusCodeSuccessful() - .fire(); - } - - private Policy buildImportedPolicy(final PolicyId policyId, - final Set allowedImportAdditions) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - private Policy buildImportingPolicy(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .build(); - } - - private Policy buildImportingPolicyWithSubjectAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - - private Policy buildImportingPolicyWithSubjectAndResourceAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject, - final Resource additionalResource) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - - private void putThingAndWaitForSearchIndex(final JsonObject thingJson) { - putThing(TestConstants.API_V_2, thingJson, V_2) - .withHeader(DittoHeaderDefinition.REQUESTED_ACKS.getKey(), - DittoAcknowledgementLabel.SEARCH_PERSISTED.toString()) - .withHeader(DittoHeaderDefinition.TIMEOUT.getKey(), "30s") - .expectingStatusCodeSuccessful() - .fire(); - } - - private Policy buildImportedPolicyWithResources(final PolicyId policyId, - final Set allowedImportAdditions, - final List defaultResources) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - defaultResources, - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - private static JsonObject buildThingJson(final ThingId thingId, final PolicyId policyId) { - return JsonObject.newBuilder() - .set("thingId", thingId.toString()) - .set("policyId", policyId.toString()) - .set("attributes", JsonObject.newBuilder() - .set("manufacturer", "ACME") - .build()) - .set("features", JsonObject.newBuilder() - .set("sensor", JsonObject.newBuilder() - .set("properties", JsonObject.newBuilder() - .set("temperature", 42) - .build()) - .build()) - .set("actuator", JsonObject.newBuilder() - .set("properties", JsonObject.newBuilder() - .set("active", true) - .build()) - .build()) - .build()) - .build(); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithTransitiveImportsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithTransitiveImportsIT.java index 2b7565a..e1e1652 100644 --- a/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithTransitiveImportsIT.java +++ b/system/src/test/java/org/eclipse/ditto/testing/system/search/security/SearchWithTransitiveImportsIT.java @@ -32,11 +32,10 @@ import org.eclipse.ditto.base.model.acks.DittoAcknowledgementLabel; import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition; import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.policies.model.AllowedImportAddition; import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; import org.eclipse.ditto.policies.model.ImportableType; import org.eclipse.ditto.policies.model.Label; import org.eclipse.ditto.policies.model.PoliciesModelFactory; @@ -89,7 +88,7 @@ public void secondUserFindsThingViaTransitiveImport() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -120,7 +119,7 @@ public void secondUserDoesNotFindThingAfterTransitiveImportsRemoved() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -157,12 +156,12 @@ public void secondUserFindsThingAfterTransitiveImportsAdded() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); // Leaf imports intermediate WITHOUT transitiveImports final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(intermediateId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() .setPolicyImport(simpleImport) .build(); @@ -203,7 +202,7 @@ public void searchIndexUpdatedWhenTemplateChangesInTransitiveChain() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -249,13 +248,12 @@ public void fourPolicyChainSearchIndexConsistency() { putPolicy(buildTemplatePolicy(globalTemplateId)).expectingHttpStatus(CREATED).fire(); // C: regional — imports D with entriesAdditions adding subject2 - putPolicy(buildIntermediatePolicy(regionalId, globalTemplateId, subject2)) + putPolicy(regionalId, buildIntermediatePolicyJson(regionalId, globalTemplateId, subject2)) .expectingHttpStatus(CREATED).fire(); // B: department — imports C with transitiveImports=["D"] final EffectedImports deptEffected = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(globalTemplateId)); final PolicyImport deptImport = PoliciesModelFactory.newPolicyImport(regionalId, deptEffected); final Policy departmentPolicy = buildAdminOnlyPolicy(departmentId).toBuilder() @@ -265,8 +263,7 @@ public void fourPolicyChainSearchIndexConsistency() { // A: leaf — imports B with transitiveImports=["C"] final EffectedImports leafEffected = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(regionalId)); final PolicyImport leafImport = PoliciesModelFactory.newPolicyImport(departmentId, leafEffected); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -333,26 +330,56 @@ private Policy buildTemplatePolicy(final PolicyId policyId) { .build(); } - private Policy buildIntermediatePolicy(final PolicyId policyId, final PolicyId templateId, + private JsonObject buildIntermediatePolicyJson(final PolicyId policyId, final PolicyId templateId, final Subject additionalSubject) { - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templateId, effectedImports); - - return buildAdminOnlyPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(defaultSubject.getId().toString(), defaultSubject.toJson()) + .build()) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(additionalSubject.getId().toString(), + additionalSubject.toJson()) + .build()) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + JsonObject.newBuilder() + .set("import", templateId.toString()) + .set("entry", "DEFAULT") + .build())) + .set("importable", "implicit") + .build()) + .build()) .build(); } private Policy buildLeafPolicyWithTransitiveImports(final PolicyId leafId, final PolicyId intermediateId, final PolicyId templateId) { final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(templateId)); final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(intermediateId, effectedImports); return buildAdminOnlyPolicy(leafId).toBuilder() diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/search/things/QueryThingsWithImportsAliasesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/search/things/QueryThingsWithImportsAliasesIT.java deleted file mode 100644 index 9a57732..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/search/things/QueryThingsWithImportsAliasesIT.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.search.things; - -import static org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.testing.common.matcher.search.SearchResponseMatchers.isEmpty; -import static org.eclipse.ditto.testing.common.matcher.search.SearchResponseMatchers.isEqualTo; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import org.awaitility.Awaitility; -import org.awaitility.core.ConditionFactory; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportsAlias; -import org.eclipse.ditto.policies.model.ImportsAliases; -import org.eclipse.ditto.policies.model.ImportsAliasTarget; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.policies.model.Subjects; -import org.eclipse.ditto.testing.common.SearchIntegrationTest; -import org.eclipse.ditto.testing.common.TestingContext; -import org.eclipse.ditto.testing.common.client.oauth.AuthClient; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.things.model.ThingsModelFactory; -import org.junit.Before; -import org.junit.Test; - -/** - * Search integration tests verifying that the search index correctly reflects access granted through - * policy import aliases. When a subject is added via an alias (which fans out to entries additions targets), - * the search index must be updated so that the subject can find the Thing. - */ -public final class QueryThingsWithImportsAliasesIT extends SearchIntegrationTest { - - private static final ConditionFactory AWAITILITY_SEARCH_CONFIG = - Awaitility.await().atMost(30, TimeUnit.SECONDS).pollInterval(3, TimeUnit.SECONDS); - - private static final Label ALIAS_LABEL = Label.of("operator"); - private static final Label TARGET_LABEL_1 = Label.of("operator-reactor"); - private static final Label TARGET_LABEL_2 = Label.of("operator-turbine"); - - private AuthClient secondClient; - - @Before - public void setUp() { - final TestingContext testingContext = - TestingContext.withGeneratedMockClient(serviceEnv.getTestingContext2().getSolution(), TEST_CONFIG); - secondClient = testingContext.getOAuthClient(); - } - - @Test - public void thingNotVisibleInSearchBeforeSubjectAddedViaAlias() { - final ThingId thingId = ThingId.of(idGenerator().withRandomName()); - final PolicyId thingPolicyId = PolicyId.of(thingId); - - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - putPolicy(buildTemplatePolicy(tmplPolicyId)).fire(); - - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).fire(); - - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonFactory.newPointer("status"), JsonFactory.newValue("active")) - .build(); - persistThingAndWaitTillAvailable(thing, V_2, serviceEnv.getDefaultTestingContext()); - - // user2 should NOT see the Thing in search - searchThings(V_2) - .filter(idFilter(thingId)) - .withJWT(secondClient.getAccessToken()) - .expectingBody(isEmpty()) - .fire(); - } - - @Test - public void thingBecomesVisibleInSearchAfterSubjectAddedViaAlias() { - final ThingId thingId = ThingId.of(idGenerator().withRandomName()); - final PolicyId thingPolicyId = PolicyId.of(thingId); - - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - putPolicy(buildTemplatePolicy(tmplPolicyId)).fire(); - - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).fire(); - - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonFactory.newPointer("status"), JsonFactory.newValue("active")) - .build(); - persistThingAndWaitTillAvailable(thing, V_2, serviceEnv.getDefaultTestingContext()); - - // Add user2 via the alias — fans out to both entries additions targets - final Subject user2Subject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - putPolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString(), user2Subject) - .fire(); - - // user2 should now see the Thing in search (wait for eventual consistency) - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withJWT(secondClient.getAccessToken()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - } - - @Test - public void thingDisappearsFromSearchAfterSubjectRemovedViaAlias() { - final ThingId thingId = ThingId.of(idGenerator().withRandomName()); - final PolicyId thingPolicyId = PolicyId.of(thingId); - - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - putPolicy(buildTemplatePolicy(tmplPolicyId)).fire(); - - // Create importing policy and then add user2 via alias (instead of embedding in entriesAdditions) - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).fire(); - - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonFactory.newPointer("status"), JsonFactory.newValue("active")) - .build(); - persistThingAndWaitTillAvailable(thing, V_2, serviceEnv.getDefaultTestingContext()); - - // Add user2 via alias - final Subject user2Subject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - putPolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString(), user2Subject) - .fire(); - - // user2 can see the Thing in search (wait for eventual consistency) - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withJWT(secondClient.getAccessToken()) - .expectingBody(isEqualTo(toThingResult(thingId))) - .fire(); - - // Remove user2 via the alias - deletePolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString()) - .fire(); - - // user2 should no longer see the Thing (wait for eventual consistency) - searchThings(V_2) - .useAwaitility(AWAITILITY_SEARCH_CONFIG) - .filter(idFilter(thingId)) - .withJWT(secondClient.getAccessToken()) - .expectingBody(isEmpty()) - .fire(); - } - - // --- Helpers --- - - private static Policy buildTemplatePolicy(final PolicyId templateId) { - return PoliciesModelFactory.newPolicyBuilder(templateId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel(TARGET_LABEL_1.toString()) - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor(TARGET_LABEL_1.toString(), Set.of(AllowedImportAddition.SUBJECTS)) - .forLabel(TARGET_LABEL_2.toString()) - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor(TARGET_LABEL_2.toString(), Set.of(AllowedImportAddition.SUBJECTS)) - .build(); - } - - private static Policy buildImportingPolicyWithAlias(final PolicyId policyId, final PolicyId tmplPolicyId) { - final EntryAddition addition1 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_1, null, null); - final EntryAddition addition2 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_2, null, null); - final EntriesAdditions entriesAdditions = - PoliciesModelFactory.newEntriesAdditions(Arrays.asList(addition1, addition2)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - Arrays.asList(TARGET_LABEL_1, TARGET_LABEL_2), entriesAdditions); - final PolicyImport pImport = PoliciesModelFactory.newPolicyImport(tmplPolicyId, effectedImports); - - final List targets = Arrays.asList( - PoliciesModelFactory.newImportsAliasTarget(tmplPolicyId, TARGET_LABEL_1), - PoliciesModelFactory.newImportsAliasTarget(tmplPolicyId, TARGET_LABEL_2)); - final ImportsAlias alias = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, targets); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setPolicyImports(PoliciesModelFactory.newPolicyImports(Collections.singletonList(pImport))) - .setImportsAliases(PoliciesModelFactory.newImportsAliases(Collections.singletonList(alias))) - .build(); - } - - private static Subject defaultSubject() { - return serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryCombinedReferencesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryCombinedReferencesIT.java new file mode 100644 index 0000000..d97342e --- /dev/null +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryCombinedReferencesIT.java @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.testing.system.things.rest; + +import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; +import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; +import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; +import static org.eclipse.ditto.base.model.common.HttpStatus.OK; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; +import static org.eclipse.ditto.things.api.Permission.READ; +import static org.eclipse.ditto.things.api.Permission.WRITE; + +import java.util.List; +import java.util.Set; + +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.policies.model.AllowedImportAddition; +import org.eclipse.ditto.policies.model.ImportableType; +import org.eclipse.ditto.policies.model.PoliciesModelFactory; +import org.eclipse.ditto.policies.model.PolicyEntry; +import org.eclipse.ditto.policies.model.PolicyId; +import org.eclipse.ditto.policies.model.Subject; +import org.eclipse.ditto.testing.common.IntegrationTest; +import org.eclipse.ditto.testing.common.ResourcePathBuilder; +import org.eclipse.ditto.testing.common.TestConstants; +import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; +import org.eclipse.ditto.testing.common.matcher.PutMatcher; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for policy entries that combine both import references and local references. + * Verifies additive resolution of resources (from import refs) and subjects (from local refs) + * on the same entry, as well as reference chaining. + * + * @since 3.9.0 + */ +public final class PolicyEntryCombinedReferencesIT extends IntegrationTest { + + private PolicyId templatePolicyId; + private PolicyId importingPolicyId; + private Subject defaultSubject; + private Subject subject2; + + @Before + public void setUp() { + templatePolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("template")); + importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); + defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); + subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); + } + + @Test + public void entryWithImportAndLocalReferenceMergesBoth() { + // Template: DEFAULT grants thing:/ READ + createTemplatePolicy(templatePolicyId); + + // Importing policy: + // "shared-subjects" has subject2 (no resources) + // "consumer" has no own subjects/resources, references both import + local + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", importsJson(templatePolicyId, "DEFAULT")) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"), + localRef("shared-subjects"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access: subjects from local ref, resources from import ref + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void entryWithOwnSubjectsAndImportReference() { + // This is the canonical migration pattern from entriesAdditions: + // Entry has subject2 as own subject + import reference for resources + createTemplatePolicy(templatePolicyId); + + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", importsJson(templatePolicyId, "DEFAULT")) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access: own subject + resources from import ref + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void entryWithOwnResourcesAndLocalReference() { + // "shared-subjects" has subject2 + // "operator" has own thing:/ READ+WRITE + references shared-subjects for subjects + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("operator", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", resourcesJson("thing:/", List.of("READ", "WRITE"), List.of())) + .set("references", JsonArray.of(localRef("shared-subjects"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can read+write + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void localReferenceToEntryThatHasImportReferenceChains() { + // Template: DEFAULT grants thing:/ READ + createTemplatePolicy(templatePolicyId); + + // "import-receiver" has import ref to template DEFAULT (gets resources) + // "shared-subjects" has subject2 + // "consumer" has local ref to both import-receiver and shared-subjects + // -> gets resources (via import-receiver's resolved import ref) + subjects (via shared-subjects) + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", importsJson(templatePolicyId, "DEFAULT")) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("import-receiver", JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"))) + .build()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + localRef("import-receiver"), + localRef("shared-subjects"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access: subjects from shared-subjects, resources from import-receiver's import ref + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void removingImportRefWhileKeepingLocalRefPreservesSubjects() { + createTemplatePolicy(templatePolicyId); + + // Entry with both import ref (resources) and local ref (subjects) + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", importsJson(templatePolicyId, "DEFAULT")) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"), + localRef("shared-subjects"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Remove import ref, keep only local ref + putReferences(importingPolicyId, "consumer", JsonArray.of(localRef("shared-subjects"))) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 loses access (no resources after resolution - entry filtered out) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void twoLevelTransitiveWithImportReferencesGrantsAccess() { + // 3-policy chain: template -> intermediate -> leaf + // Template (C): DEFAULT grants thing:/ READ, importable=IMPLICIT + final PolicyId intermediateId = PolicyId.of(idGenerator().withPrefixedRandomName("intermediate")); + createTemplatePolicy(templatePolicyId); + + // Intermediate (B): imports C, has "user-access" entry with subject2 + import ref to C:DEFAULT + final JsonObject intermediateJson = JsonObject.newBuilder() + .set("policyId", intermediateId.toString()) + .set("imports", importsJson(templatePolicyId, "DEFAULT")) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"))) + .set("importable", "implicit") + .build()) + .build()) + .build(); + putPolicy(intermediateId, intermediateJson).expectingHttpStatus(CREATED).fire(); + + // Leaf (A): imports B with transitiveImports=[C], has entry with import ref to B:user-access + final JsonObject leafJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(intermediateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("user-access").build()) + .set("transitiveImports", JsonFactory.newArrayBuilder() + .add(templatePolicyId.toString()).build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .build()) + .build(); + putPolicy(importingPolicyId, leafJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access through the transitive chain + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void threeImportRefsToEntriesFromDifferentImportedPolicies() { + // Template-A: grants thing:/attributes READ + final PolicyId templateAId = PolicyId.of(idGenerator().withPrefixedRandomName("templateA")); + final PolicyEntry adminEntryA = PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + final PolicyEntry attrEntry = PoliciesModelFactory.newPolicyEntry("ATTR_ACCESS", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicy(PoliciesModelFactory.newPolicyBuilder(templateAId) + .set(adminEntryA).set(attrEntry).build()) + .expectingHttpStatus(CREATED).fire(); + + // Template-B: grants thing:/features READ + final PolicyId templateBId = PolicyId.of(idGenerator().withPrefixedRandomName("templateB")); + final PolicyEntry adminEntryB = PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + final PolicyEntry featEntry = PoliciesModelFactory.newPolicyEntry("FEAT_ACCESS", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/features"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicy(PoliciesModelFactory.newPolicyBuilder(templateBId) + .set(adminEntryB).set(featEntry).build()) + .expectingHttpStatus(CREATED).fire(); + + // Importing policy: imports both templates, entry references both + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateAId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("ATTR_ACCESS").build()) + .build()) + .set(templateBId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("FEAT_ACCESS").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("combined-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templateAId, "ATTR_ACCESS"), + importRef(templateBId, "FEAT_ACCESS"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("key", "value").build()) + .set("features", JsonObject.newBuilder() + .set("sensor", JsonObject.newBuilder() + .set("properties", JsonObject.newBuilder() + .set("temp", 22).build()) + .build()) + .build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can read attributes (from template-A) + getAttributes(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // subject2 can read features (from template-B) + getFeatures(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Helpers ---- + + private void createTemplatePolicy(final PolicyId policyId) { + final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicy(PoliciesModelFactory.newPolicyBuilder(policyId) + .set(adminEntry).set(defaultEntry).build()) + .expectingHttpStatus(CREATED).fire(); + } + + private void putThingWithPolicy(final String thingId, final PolicyId policyId) { + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", policyId.toString()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + } + + private JsonObject buildAdminEntryJson() { + return JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build(); + } + + private static JsonObject importsJson(final PolicyId templateId, final String... entries) { + final var arrayBuilder = JsonFactory.newArrayBuilder(); + for (final String entry : entries) { + arrayBuilder.add(entry); + } + return JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", arrayBuilder.build()) + .build()) + .build(); + } + + private static JsonObject subjectsJson(final Subject subject) { + return JsonObject.newBuilder() + .set(subject.getId().toString(), subject.toJson()) + .build(); + } + + private static JsonObject resourcesJson(final String path, final List grant, + final List revoke) { + return JsonObject.newBuilder() + .set(path, JsonObject.newBuilder() + .set("grant", toJsonArray(grant)) + .set("revoke", toJsonArray(revoke)) + .build()) + .build(); + } + + private static JsonArray toJsonArray(final List strings) { + final var builder = JsonFactory.newArrayBuilder(); + strings.forEach(builder::add); + return builder.build(); + } + + private static JsonObject importRef(final PolicyId policyId, final String entryLabel) { + return JsonObject.newBuilder() + .set("import", policyId.toString()) + .set("entry", entryLabel) + .build(); + } + + private static JsonObject localRef(final String entryLabel) { + return JsonObject.newBuilder() + .set("entry", entryLabel) + .build(); + } + + private static PutMatcher putReferences(final PolicyId policyId, final String label, + final JsonArray references) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return put(dittoUrl(TestConstants.API_V_2, path), references.toString()) + .withLogging(LOGGER, "References"); + } + +} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportReferencesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportReferencesIT.java new file mode 100644 index 0000000..e208e7d --- /dev/null +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportReferencesIT.java @@ -0,0 +1,910 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.testing.system.things.rest; + +import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; +import static org.eclipse.ditto.base.model.common.HttpStatus.FORBIDDEN; +import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; +import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; +import static org.eclipse.ditto.base.model.common.HttpStatus.OK; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; +import static org.eclipse.ditto.things.api.Permission.READ; +import static org.eclipse.ditto.things.api.Permission.WRITE; + +import java.util.List; +import java.util.Set; + +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.policies.model.AllowedImportAddition; +import org.eclipse.ditto.policies.model.EffectedImports; +import org.eclipse.ditto.policies.model.ImportableType; +import org.eclipse.ditto.policies.model.Label; +import org.eclipse.ditto.policies.model.PoliciesModelFactory; +import org.eclipse.ditto.policies.model.Policy; +import org.eclipse.ditto.policies.model.PolicyEntry; +import org.eclipse.ditto.policies.model.PolicyId; +import org.eclipse.ditto.policies.model.PolicyImport; +import org.eclipse.ditto.policies.model.Resource; +import org.eclipse.ditto.policies.model.Subject; +import org.eclipse.ditto.testing.common.IntegrationTest; +import org.eclipse.ditto.testing.common.ResourcePathBuilder; +import org.eclipse.ditto.testing.common.TestConstants; +import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; +import org.eclipse.ditto.testing.common.matcher.GetMatcher; +import org.eclipse.ditto.testing.common.matcher.PutMatcher; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for import references ({@code {"import":"policyId","entry":"label"}}) on policy entries. + * Verifies that an entry with an import reference inherits resources and namespaces from the referenced + * imported entry, while subjects remain local to the referencing entry. + *

+ * Migrated from {@code PolicyImportEntriesAdditionsIT} and {@code ThingsWithImportedPoliciesEntriesAdditionsIT}. + *

+ * + * @since 3.9.0 + */ +public final class PolicyEntryImportReferencesIT extends IntegrationTest { + + private PolicyId templatePolicyId; + private PolicyId importingPolicyId; + private Subject defaultSubject; + private Subject subject2; + + @Before + public void setUp() { + templatePolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("template")); + importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); + defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); + subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); + } + + // ---- Thing access via import reference ---- + + @Test + public void secondUserGainsThingAccessViaImportReference() { + // Template: DEFAULT grants thing:/ READ, allows subject additions + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + + // Importing policy: "user-access" entry has subject2 + import reference to DEFAULT + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + // Create a thing + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access the thing via inherited resources from import reference + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void secondUserLosesAccessWhenImportReferenceRemoved() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Remove the import reference + deleteReferences(importingPolicyId, "user-access") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 loses access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void importReferenceInheritsResourcesNotSubjects() { + createTemplatePolicy(templatePolicyId, Set.of()); + + // Importing policy: "user-access" entry has NO subjects, only import reference + final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( + List.of(Label.of("DEFAULT"))); + final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templatePolicyId, effectedImports); + + // Entry with import reference but empty subjects + final JsonObject policyJson = buildImportingPolicyJson(importingPolicyId, templatePolicyId, + JsonObject.empty(), // no subjects + JsonArray.of(importRef(templatePolicyId, "DEFAULT"))); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 cannot access (entry has resources from import ref but no subjects) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- allowedImportAdditions enforcement ---- + + @Test + public void importRefWithSubjectAdditionsAllowed() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + getPolicy(importingPolicyId).expectingHttpStatus(OK).fire(); + } + + @Test + public void importRefWithResourceAdditionsAllowed() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.RESOURCES)); + + // Entry with extra resource + import reference + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/attributes", List.of("READ")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + getPolicy(importingPolicyId).expectingHttpStatus(OK).fire(); + } + + @Test + public void importRefWithSubjectAndResourceAdditionsAllowed() { + createTemplatePolicy(templatePolicyId, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/attributes", List.of("READ")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + getPolicy(importingPolicyId).expectingHttpStatus(OK).fire(); + } + + @Test + public void resourceAdditionGrantsWriteAccess() { + createTemplatePolicy(templatePolicyId, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + + // Entry adds WRITE resource + subject2 + import reference + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/", List.of("WRITE")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ (from template) and WRITE (from own resource addition) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void templateRevokePreservedWhenResourceAdditionsOverlap() { + // Template: DEFAULT grants READ, revokes WRITE, allows both additions + final PolicyEntry adminEntry = buildAdminEntry(); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of(WRITE)))), + ImportableType.IMPLICIT, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry).set(defaultEntry).build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + + // Entry adds subject2 + WRITE grant on thing:/ + import reference + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/", List.of("WRITE")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // subject2 cannot WRITE (template revoke overrides) + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(FORBIDDEN) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Template changes propagate ---- + + @Test + public void templatePermissionChangePropagatesToImportReference() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ but not WRITE + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(FORBIDDEN) + .fire(); + + // Update template: grant READ+WRITE + final PolicyEntry updatedDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicyEntry(templatePolicyId, updatedDefaultEntry) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 can now WRITE + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void reducingAllowedImportAdditionsRevokesResourceAdditionEffect() { + createTemplatePolicy(templatePolicyId, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/", List.of("WRITE")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ + WRITE + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Reduce allowedImportAdditions: remove RESOURCES + final PolicyEntry reducedEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicyEntry(templatePolicyId, reducedEntry).expectingHttpStatus(NO_CONTENT).fire(); + + // subject2 can still READ + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // subject2 can no longer WRITE + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "updated").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(FORBIDDEN) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void templateRevokeAddedAfterImportOverridesResourceAdditionGrant() { + // Template: DEFAULT grants READ, allows both additions + final PolicyEntry adminEntry = buildAdminEntry(); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry).set(defaultEntry).build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + + // Entry with subject2 + WRITE resource + import ref + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/", List.of("WRITE")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can WRITE initially + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Template adds WRITE revoke + final PolicyEntry updatedDefault = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of(WRITE)))), + ImportableType.IMPLICIT, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + putPolicyEntry(templatePolicyId, updatedDefault).expectingHttpStatus(NO_CONTENT).fire(); + + // subject2 WRITE now forbidden + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "updated").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(FORBIDDEN) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Template deletion ---- + + @Test + public void templateEntryDeletionRevokesImportReferenceAccess() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Delete template DEFAULT entry + deletePolicyEntry(templatePolicyId, "DEFAULT").expectingHttpStatus(NO_CONTENT).fire(); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void templatePolicyDeletionRevokesImportReferenceAccess() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Delete entire template policy + deletePolicy(templatePolicyId).expectingHttpStatus(NO_CONTENT).fire(); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Multiple labels / importers ---- + + @Test + public void importRefsForMultipleImportedLabels() { + // Template: DEFAULT (thing:/ READ) and EXTRA (thing:/ WRITE), both allow subject additions + final PolicyEntry adminEntry = buildAdminEntry(); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + final PolicyEntry extraEntry = PoliciesModelFactory.newPolicyEntry("EXTRA", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of()))), + ImportableType.EXPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry).set(defaultEntry).set(extraEntry).build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + + // Two entries in importing policy, each with import ref to different template entries + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templatePolicyId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").add("EXTRA").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("read-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "DEFAULT"))) + .build()) + .set("write-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + importRef(templatePolicyId, "EXTRA"))) + .build()) + .build()) + .build(); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ (from DEFAULT ref) and WRITE (from EXTRA ref) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void multipleImportersFromSameTemplateAreIndependent() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + + final PolicyId importingPolicyId2 = PolicyId.of(idGenerator().withPrefixedRandomName("importing2")); + + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + createImportingPolicyWithImportRef(importingPolicyId2, templatePolicyId, subject2); + + final String thingId1 = importingPolicyId.toString(); + final String thingId2 = importingPolicyId2.toString(); + putThingWithPolicy(thingId1, importingPolicyId); + putThingWithPolicy(thingId2, importingPolicyId2); + + // subject2 can READ both + getThing(TestConstants.API_V_2, thingId1) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + getThing(TestConstants.API_V_2, thingId2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Change template: importable=NEVER + final PolicyEntry neverEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of(AllowedImportAddition.SUBJECTS)); + putPolicyEntry(templatePolicyId, neverEntry).expectingHttpStatus(NO_CONTENT).fire(); + + // Both lose access + getThing(TestConstants.API_V_2, thingId1) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + getThing(TestConstants.API_V_2, thingId2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId1).expectingHttpStatus(NO_CONTENT).fire(); + deleteThing(TestConstants.API_V_2, thingId2).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void subjectRetainsAccessFromOwnEntryWhenImportReferenceRemoved() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + // Add a direct entry for subject2 as well + final PolicyEntry directEntry = PoliciesModelFactory.newPolicyEntry("DIRECT_USER2", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, directEntry).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Remove import reference + deleteReferences(importingPolicyId, "user-access") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 still has access via DIRECT_USER2 + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Fine-grained resource paths ---- + + @Test + public void resourceAdditionRespectsSubPathGranularity() { + createTemplatePolicy(templatePolicyId, + Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); + + // Entry adds WRITE on thing:/attributes only (not features) + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, templatePolicyId, subject2, "thing:/attributes", List.of("WRITE")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("key", "value").build()) + .set("features", JsonObject.newBuilder() + .set("sensor", JsonObject.newBuilder() + .set("properties", JsonObject.newBuilder() + .set("value", 42).build()) + .build()) + .build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can READ whole thing (from template) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // subject2 can WRITE attributes + putAttributes(TestConstants.API_V_2, thingId, + JsonObject.newBuilder().set("key", "updated").build().toString()) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 cannot WRITE features + putFeatures(TestConstants.API_V_2, thingId, + JsonObject.newBuilder() + .set("sensor", JsonObject.newBuilder() + .set("properties", JsonObject.newBuilder() + .set("value", 99).build()) + .build()) + .build().toString()) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(FORBIDDEN) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void resourceAdditionWithoutSubjectAdditionAppliesToTemplateSubjects() { + // Template: DEFAULT has subject2 as subject with thing:/ READ + final PolicyEntry adminEntry = buildAdminEntry(); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of(AllowedImportAddition.RESOURCES)); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry).set(defaultEntry).build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + + // Entry with WRITE resource, no subjects, import ref inherits template's resources + final JsonObject policyJson = buildImportingPolicyJson(importingPolicyId, templatePolicyId, + JsonObject.empty(), + JsonArray.of(importRef(templatePolicyId, "DEFAULT")), + JsonObject.newBuilder() + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder().add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + // subject2 can READ (template subject) and WRITE (resource addition) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", importingPolicyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void changingImportableToNeverRevokesReferencedAccess() { + createTemplatePolicy(templatePolicyId, Set.of(AllowedImportAddition.SUBJECTS)); + createImportingPolicyWithImportRef(importingPolicyId, templatePolicyId, subject2); + + final String thingId = importingPolicyId.toString(); + putThingWithPolicy(thingId, importingPolicyId); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Change template to importable=NEVER + final PolicyEntry neverEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(templatePolicyId, neverEntry).expectingHttpStatus(NO_CONTENT).fire(); + + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Helpers ---- + + private void createTemplatePolicy(final PolicyId policyId, + final Set allowedAdditions) { + + final PolicyEntry adminEntry = buildAdminEntry(); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, allowedAdditions); + final Policy policy = PoliciesModelFactory.newPolicyBuilder(policyId) + .set(adminEntry).set(defaultEntry).build(); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + } + + private void createImportingPolicyWithImportRef(final PolicyId policyId, + final PolicyId templateId, final Subject localSubject) { + + final JsonObject policyJson = buildImportingPolicyJson(policyId, templateId, + subjectsJson(localSubject), + JsonArray.of(importRef(templateId, "DEFAULT"))); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + } + + private PolicyEntry buildAdminEntry() { + return PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + } + + private void putThingWithPolicy(final String thingId, final PolicyId policyId) { + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", policyId.toString()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + } + + private JsonObject buildImportingPolicyJson(final PolicyId policyId, final PolicyId templateId, + final JsonObject userAccessSubjects, final JsonArray references) { + return buildImportingPolicyJson(policyId, templateId, userAccessSubjects, references, JsonObject.empty()); + } + + private JsonObject buildImportingPolicyJson(final PolicyId policyId, final PolicyId templateId, + final JsonObject userAccessSubjects, final JsonArray references, + final JsonObject extraResources) { + + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", userAccessSubjects) + .set("resources", extraResources) + .set("references", references) + .build()) + .build()) + .build(); + } + + private JsonObject buildImportingPolicyJsonWithExtraResource(final PolicyId policyId, + final PolicyId templateId, final Subject localSubject, + final String resourcePath, final List grantedPerms) { + + final JsonObject resourcesJson = JsonObject.newBuilder() + .set(resourcePath, JsonObject.newBuilder() + .set("grant", toJsonArray(grantedPerms)) + .set("revoke", JsonArray.empty()) + .build()) + .build(); + + return buildImportingPolicyJson(policyId, templateId, + subjectsJson(localSubject), + JsonArray.of(importRef(templateId, "DEFAULT")), + resourcesJson); + } + + private JsonObject buildAdminEntryJson() { + return JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build(); + } + + private static JsonObject subjectsJson(final Subject subject) { + return JsonObject.newBuilder() + .set(subject.getId().toString(), subject.toJson()) + .build(); + } + + private static JsonArray toJsonArray(final List strings) { + final var builder = JsonFactory.newArrayBuilder(); + strings.forEach(builder::add); + return builder.build(); + } + + private static JsonObject importRef(final PolicyId policyId, final String entryLabel) { + return JsonObject.newBuilder() + .set("import", policyId.toString()) + .set("entry", entryLabel) + .build(); + } + + private static GetMatcher getReferences(final PolicyId policyId, final String label) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return get(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "References"); + } + + private static PutMatcher putReferences(final PolicyId policyId, final String label, + final JsonArray references) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return put(dittoUrl(TestConstants.API_V_2, path), references.toString()) + .withLogging(LOGGER, "References"); + } + + private static DeleteMatcher deleteReferences(final PolicyId policyId, final String label) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "References"); + } + +} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportableSubResourcesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportableSubResourcesIT.java index 6ca8da7..3785b2b 100644 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportableSubResourcesIT.java +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryImportableSubResourcesIT.java @@ -26,12 +26,10 @@ import java.util.Set; import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; import org.eclipse.ditto.policies.model.ImportableType; import org.eclipse.ditto.policies.model.Label; import org.eclipse.ditto.policies.model.PoliciesModelFactory; @@ -39,11 +37,11 @@ import org.eclipse.ditto.policies.model.PolicyEntry; import org.eclipse.ditto.policies.model.PolicyId; import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Resource; import org.eclipse.ditto.policies.model.Subject; import org.eclipse.ditto.testing.common.IntegrationTest; import org.eclipse.ditto.testing.common.ResourcePathBuilder; import org.eclipse.ditto.testing.common.TestConstants; +import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; import org.eclipse.ditto.testing.common.matcher.GetMatcher; import org.eclipse.ditto.testing.common.matcher.PutMatcher; import org.junit.Before; @@ -128,10 +126,10 @@ public void changingImportableToNeverRevokesThingAccess() { Set.of(AllowedImportAddition.SUBJECTS)); putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - // Create importing policy with subject2 added via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); + // Create importing policy with subject2 + import reference to DEFAULT + putPolicy(importingPolicyId, buildImportingPolicyJsonWithImportRef( + importingPolicyId, importedPolicyId, subject2)) + .expectingHttpStatus(CREATED).fire(); // Create a thing with the importing policy final String thingId = importingPolicyId.toString(); @@ -167,23 +165,30 @@ public void changingImportableToNeverRevokesThingAccess() { } @Test - public void addingAllowedImportAdditionsEnablesSubjectAdditions() { + public void addingAllowedImportAdditionsEnablesImportRefWithSubjects() { // Create imported policy WITHOUT allowedImportAdditions (but IMPLICIT importable) final Policy importedPolicy = buildImportedPolicyWithoutAllowedAdditions(importedPolicyId); putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - // Create importing policy with a simple import (no additions) + // Create importing policy with import and a user-access entry (no references yet) final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); final Policy importingPolicy = buildImportingPolicy(importingPolicyId) .toBuilder().setPolicyImport(simpleImport).build(); putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - // Try to modify the import to add subject additions - should be rejected - final PolicyImport importWithAdditions = buildImportWithSubjectAdditions(importedPolicyId, subject2); - putPolicyImport(importingPolicyId, importWithAdditions) + // Add a user-access entry + final PolicyEntry userEntry = PoliciesModelFactory.newPolicyEntry("user-access", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, userEntry).expectingHttpStatus(CREATED).fire(); + + // Try to add import reference with subject - should be rejected (no allowedAdditions) + final JsonArray refs = JsonArray.of(importRef(importedPolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") .fire(); // Add allowedImportAdditions=["subjects"] to the imported policy's DEFAULT entry @@ -192,8 +197,8 @@ public void addingAllowedImportAdditionsEnablesSubjectAdditions() { .expectingHttpStatus(NO_CONTENT) .fire(); - // Now modifying the import to add subject additions should succeed - putPolicyImport(importingPolicyId, importWithAdditions) + // Now adding the import reference should succeed + putReferences(importingPolicyId, "user-access", refs) .expectingHttpStatus(NO_CONTENT) .fire(); } @@ -204,7 +209,7 @@ public void addingAllowedImportAdditionsViaSubResourceEnablesThingAccess() { final Policy importedPolicy = buildImportedPolicyWithoutAllowedAdditions(importedPolicyId); putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - // Create importing policy with a simple import (no additions) + // Create importing policy with a simple import (no references yet) final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); final Policy importingPolicy = buildImportingPolicy(importingPolicyId) @@ -233,9 +238,15 @@ public void addingAllowedImportAdditionsViaSubResourceEnablesThingAccess() { .expectingHttpStatus(NO_CONTENT) .fire(); - // Now add subject2 via entriesAdditions on the import - final PolicyImport importWithAdditions = buildImportWithSubjectAdditions(importedPolicyId, subject2); - putPolicyImport(importingPolicyId, importWithAdditions) + // Add a user-access entry with subject2 + import reference + final PolicyEntry userEntry = PoliciesModelFactory.newPolicyEntry("user-access", + List.of(subject2), + List.of(), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, userEntry).expectingHttpStatus(CREATED).fire(); + + final JsonArray refs = JsonArray.of(importRef(importedPolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) .expectingHttpStatus(NO_CONTENT) .fire(); @@ -252,62 +263,50 @@ public void addingAllowedImportAdditionsViaSubResourceEnablesThingAccess() { } @Test - public void removingAllowedImportAdditionsRejectsSubjectAdditions() { + public void removingAllowedImportAdditionsRejectsNewImportRefWithSubjects() { // Create imported policy WITH allowedImportAdditions=["subjects"] final Policy importedPolicy = buildImportedPolicy(importedPolicyId, Set.of(AllowedImportAddition.SUBJECTS)); putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - // Create importing policy with a simple import (no additions) + // Create importing policy with import final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); final Policy importingPolicy = buildImportingPolicy(importingPolicyId) .toBuilder().setPolicyImport(simpleImport).build(); putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); + // Add a user-access entry + final PolicyEntry userEntry = PoliciesModelFactory.newPolicyEntry("user-access", + List.of(subject2), + List.of(), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, userEntry).expectingHttpStatus(CREATED).fire(); + // Remove allowedImportAdditions from the imported policy's DEFAULT entry putPolicyEntryAllowedImportAdditions(importedPolicyId, "DEFAULT", JsonArray.empty()) .expectingHttpStatus(NO_CONTENT) .fire(); - // Try to modify the import to add subject additions - should be rejected - final PolicyImport importWithAdditions = buildImportWithSubjectAdditions(importedPolicyId, subject2); - putPolicyImport(importingPolicyId, importWithAdditions) + // Try to add import reference - should be rejected (no allowed additions) + final JsonArray refs = JsonArray.of(importRef(importedPolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") .fire(); } @Test - public void removingResourcesFromAllowedAdditionsRejectsNewResourceAdditions() { + public void removingResourcesFromAllowedAdditionsRejectsNewResourceOnEntry() { // Create imported policy with allowedImportAdditions=["subjects","resources"] final Policy importedPolicy = buildImportedPolicy(importedPolicyId, Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - // Create importing policy with a simple import (no additions) - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Verify that adding resource additions currently works - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition resourceAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), null, - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions resourceAdditions = PoliciesModelFactory.newEntriesAdditions( - List.of(resourceAddition)); - final EffectedImports effectedWithResources = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), resourceAdditions); - final PolicyImport importWithResources = PoliciesModelFactory.newPolicyImport( - importedPolicyId, effectedWithResources); - putPolicyImport(importingPolicyId, importWithResources) - .expectingHttpStatus(NO_CONTENT) - .fire(); + // Create importing policy with import ref + extra resource on the entry + final JsonObject policyJson = buildImportingPolicyJsonWithExtraResource( + importingPolicyId, importedPolicyId, subject2, "thing:/attributes", List.of("READ")); + putPolicy(importingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); // Remove "resources" from allowedImportAdditions, keeping only "subjects" putPolicyEntryAllowedImportAdditions(importedPolicyId, "DEFAULT", @@ -315,11 +314,9 @@ public void removingResourcesFromAllowedAdditionsRejectsNewResourceAdditions() { .expectingHttpStatus(NO_CONTENT) .fire(); - // Now attempting to update the import with resource additions should be rejected - putPolicyImport(importingPolicyId, importWithResources) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); + // The existing entry's extra resource should no longer be effective at enforcement time + // Verify via thing access: subject2 can still READ (from template) but no extra resources + getPolicy(importingPolicyId).expectingHttpStatus(OK).fire(); } // --- Helper methods for building policies --- @@ -366,33 +363,91 @@ private Policy buildImportingPolicy(final PolicyId policyId) { .build(); } - private Policy buildImportingPolicyWithSubjectAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject) { + private JsonObject buildImportingPolicyJsonWithImportRef(final PolicyId policyId, + final PolicyId templateId, final Subject localSubject) { + + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", permJson(List.of("READ", "WRITE"))) + .set("thing:/", permJson(List.of("READ", "WRITE"))) + .build()) + .set("importable", "never") + .build()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(localSubject)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of(importRef(templateId, "DEFAULT"))) + .build()) + .build()) + .build(); + } - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); + private JsonObject buildImportingPolicyJsonWithExtraResource(final PolicyId policyId, + final PolicyId templateId, final Subject localSubject, + final String resourcePath, final List grantedPerms) { + + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", permJson(List.of("READ", "WRITE"))) + .set("thing:/", permJson(List.of("READ", "WRITE"))) + .build()) + .set("importable", "never") + .build()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(localSubject)) + .set("resources", JsonObject.newBuilder() + .set(resourcePath, permJson(grantedPerms)) + .build()) + .set("references", JsonArray.of(importRef(templateId, "DEFAULT"))) + .build()) + .build()) + .build(); + } - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) + private static JsonObject subjectsJson(final Subject subject) { + return JsonObject.newBuilder() + .set(subject.getId().toString(), subject.toJson()) .build(); } - private static PolicyImport buildImportWithSubjectAdditions(final PolicyId importedPolicyId, - final Subject additionalSubject) { + private static JsonObject permJson(final List grant) { + return JsonObject.newBuilder() + .set("grant", toJsonArray(grant)) + .set("revoke", JsonArray.empty()) + .build(); + } - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); + private static JsonArray toJsonArray(final List strings) { + final var builder = JsonFactory.newArrayBuilder(); + strings.forEach(builder::add); + return builder.build(); + } - return PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); + private static JsonObject importRef(final PolicyId policyId, final String entryLabel) { + return JsonObject.newBuilder() + .set("import", policyId.toString()) + .set("entry", entryLabel) + .build(); } // --- Helper methods for sub-resource HTTP operations --- @@ -429,4 +484,12 @@ private static PutMatcher putPolicyEntryAllowedImportAdditions(final CharSequenc .withLogging(LOGGER, "PolicyEntryAllowedImportAdditions"); } + private static PutMatcher putReferences(final PolicyId policyId, final String label, + final JsonArray references) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return put(dittoUrl(TestConstants.API_V_2, path), references.toString()) + .withLogging(LOGGER, "References"); + } + } diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryLocalReferencesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryLocalReferencesIT.java new file mode 100644 index 0000000..4a06de3 --- /dev/null +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryLocalReferencesIT.java @@ -0,0 +1,506 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.testing.system.things.rest; + +import static org.eclipse.ditto.base.model.common.HttpStatus.CONFLICT; +import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; +import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; +import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; +import static org.eclipse.ditto.base.model.common.HttpStatus.OK; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; +import static org.eclipse.ditto.things.api.Permission.READ; +import static org.eclipse.ditto.things.api.Permission.WRITE; + +import java.util.List; + +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.json.JsonValue; +import org.eclipse.ditto.policies.model.ImportableType; +import org.eclipse.ditto.policies.model.PoliciesModelFactory; +import org.eclipse.ditto.policies.model.Policy; +import org.eclipse.ditto.policies.model.PolicyId; +import org.eclipse.ditto.policies.model.Subject; +import org.eclipse.ditto.testing.common.IntegrationTest; +import org.eclipse.ditto.testing.common.ResourcePathBuilder; +import org.eclipse.ditto.testing.common.TestConstants; +import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; +import org.eclipse.ditto.testing.common.matcher.GetMatcher; +import org.eclipse.ditto.testing.common.matcher.PutMatcher; +import org.eclipse.ditto.things.model.Thing; +import org.eclipse.ditto.things.model.ThingId; +import org.eclipse.ditto.things.model.ThingsModelFactory; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for local references ({@code {"entry":"label"}}) on policy entries. + * Verifies that a local reference inherits subjects, resources, and namespaces from the referenced + * entry within the same policy. Replaces the subject fan-out use case previously covered by + * {@code importsAliases}. + *

+ * Migrated from {@code PolicyImportsAliasesIT}. + *

+ * + * @since 3.9.0 + */ +public final class PolicyEntryLocalReferencesIT extends IntegrationTest { + + private PolicyId policyId; + private Subject defaultSubject; + private Subject subject2; + + @Before + public void setUp() { + policyId = PolicyId.of(idGenerator().withPrefixedRandomName("localref")); + defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); + subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); + } + + // ---- Inheritance behavior ---- + + @Test + public void localReferenceInheritsSubjectsFromReferencedEntry() { + // "shared-subjects" has subject2 + thing:/ READ + // "consumer" has defaultSubject + thing:/ WRITE + references shared-subjects + final JsonObject policyJson = buildPolicyJsonWithLocalRef(policyId, + subjectsJson(subject2), + resourcesJson("thing:/", List.of("READ"), List.of()), + subjectsJson(defaultSubject), + resourcesJson("thing:/", List.of("WRITE"), List.of()), + "shared-subjects"); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = policyId.toString(); + putThingWithPolicy(thingId, policyId); + + // subject2 can access (inherited subjects from shared-subjects into consumer's resolution) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void localReferenceInheritsResourcesFromReferencedEntry() { + // "resource-provider" has defaultSubject + thing:/ WRITE + // "consumer" has subject2 + thing:/ READ + references resource-provider + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("resource-provider", JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", resourcesJson("thing:/", List.of("WRITE"), List.of())) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", resourcesJson("thing:/", List.of("READ"), List.of())) + .set("references", JsonArray.of(localRef("resource-provider"))) + .build()) + .build()) + .build(); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = policyId.toString(); + putThingWithPolicy(thingId, policyId); + + // subject2 can READ (own) and WRITE (inherited from resource-provider) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putThing(TestConstants.API_V_2, + JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", policyId.toString()) + .set("attributes", JsonObject.newBuilder().set("test", "value").build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + @Test + public void localReferenceFansOutSubjectsToMultipleEntries() { + // "shared-subjects" has subject2 (no resources) + // "entry-read" has thing:/ READ + references shared-subjects + // "entry-write" has thing:/features WRITE + references shared-subjects + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("entry-read", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", resourcesJson("thing:/", List.of("READ"), List.of())) + .set("references", JsonArray.of(localRef("shared-subjects"))) + .build()) + .set("entry-write", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", resourcesJson("thing:/features", List.of("WRITE"), List.of())) + .set("references", JsonArray.of(localRef("shared-subjects"))) + .build()) + .build()) + .build(); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = policyId.toString(); + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", policyId.toString()) + .set("features", JsonObject.newBuilder() + .set("sensor", JsonObject.newBuilder() + .set("properties", JsonObject.newBuilder() + .set("value", 42).build()) + .build()) + .build()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can READ (from entry-read) and WRITE features (from entry-write) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + putFeatures(TestConstants.API_V_2, thingId, + JsonObject.newBuilder() + .set("sensor", JsonObject.newBuilder() + .set("properties", JsonObject.newBuilder() + .set("value", 99).build()) + .build()) + .build().toString()) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Dynamic subject management ---- + + @Test + public void subjectAddedToReferencedEntryGrantsAccess() { + final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("localRefGrant")); + final PolicyId thingPolicyId = PolicyId.of(thingId); + + // "shared-subjects" starts with defaultSubject only + // "consumer" has thing:/ READ + references shared-subjects + final JsonObject policyJson = buildPolicyJsonWithLocalRef(thingPolicyId, + subjectsJson(defaultSubject), + resourcesJson("thing:/", List.of("READ"), List.of()), + JsonObject.empty(), + resourcesJson("thing:/", List.of("READ"), List.of()), + "shared-subjects"); + putPolicy(thingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final Thing thing = ThingsModelFactory.newThingBuilder() + .setId(thingId) + .setPolicyId(thingPolicyId) + .setAttribute(JsonPointer.of("status"), JsonValue.of("active")) + .build(); + putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 cannot access yet + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + + // Add subject2 to shared-subjects + putPolicyEntrySubject(thingPolicyId, "shared-subjects", + subject2.getId().toString(), subject2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can now access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void subjectRemovedFromReferencedEntryRevokesAccess() { + final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("localRefRevoke")); + final PolicyId thingPolicyId = PolicyId.of(thingId); + + // "shared-subjects" has both defaultSubject and subject2 + final JsonObject bothSubjects = JsonObject.newBuilder() + .set(defaultSubject.getId().toString(), defaultSubject.toJson()) + .set(subject2.getId().toString(), subject2.toJson()) + .build(); + final JsonObject policyJson = buildPolicyJsonWithLocalRef(thingPolicyId, + bothSubjects, + resourcesJson("thing:/", List.of("READ"), List.of()), + JsonObject.empty(), + resourcesJson("thing:/", List.of("READ"), List.of()), + "shared-subjects"); + putPolicy(thingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final Thing thing = ThingsModelFactory.newThingBuilder() + .setId(thingId) + .setPolicyId(thingPolicyId) + .setAttribute(JsonPointer.of("status"), JsonValue.of("active")) + .build(); + putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + // Remove subject2 from shared-subjects + deletePolicyEntrySubject(thingPolicyId, "shared-subjects", + subject2.getId().toString()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // subject2 can no longer access + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NOT_FOUND) + .fire(); + } + + @Test + public void subjectAddedViaLocalReferenceCanWriteWhenEntryGrantsWrite() { + final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("localRefWrite")); + final PolicyId thingPolicyId = PolicyId.of(thingId); + + // "shared-subjects" starts with defaultSubject only + // "consumer" has thing:/ READ+WRITE + references shared-subjects + final JsonObject policyJson = buildPolicyJsonWithLocalRef(thingPolicyId, + subjectsJson(defaultSubject), + JsonObject.empty(), + JsonObject.empty(), + resourcesJson("thing:/", List.of("READ", "WRITE"), List.of()), + "shared-subjects"); + putPolicy(thingPolicyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final Thing thing = ThingsModelFactory.newThingBuilder() + .setId(thingId) + .setPolicyId(thingPolicyId) + .setAttribute(JsonPointer.of("counter"), JsonValue.of(0)) + .build(); + putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + + // Add subject2 to shared-subjects + putPolicyEntrySubject(thingPolicyId, "shared-subjects", + subject2.getId().toString(), subject2) + .expectingHttpStatus(CREATED) + .fire(); + + // subject2 can write + final String attributePath = ResourcePathBuilder.forThing(thingId).attribute("counter").toString(); + put(dittoUrl(TestConstants.API_V_2, attributePath), "42") + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Verify write took effect + get(dittoUrl(TestConstants.API_V_2, attributePath)) + .expectingBody(containsCharSequence("42")) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void multipleLocalReferencesAreMergedAdditively() { + // "subjects-entry" has subject2, no resources + // "resources-entry" has no subjects, thing:/ READ + // "consumer" references both -> gets subject2 + thing:/ READ + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("subjects-entry", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .build()) + .set("resources-entry", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", resourcesJson("thing:/", List.of("READ"), List.of())) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", JsonObject.empty()) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of( + localRef("subjects-entry"), + localRef("resources-entry"))) + .build()) + .build()) + .build(); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + final String thingId = policyId.toString(); + putThingWithPolicy(thingId, policyId); + + // subject2 can access (subjects from subjects-entry + resources from resources-entry) + getThing(TestConstants.API_V_2, thingId) + .withConfiguredAuth(serviceEnv.getTestingContext2()) + .expectingHttpStatus(OK) + .fire(); + + deleteThing(TestConstants.API_V_2, thingId).expectingHttpStatus(NO_CONTENT).fire(); + } + + // ---- Referential integrity ---- + + @Test + public void deleteReferencedEntryIsRejectedWhileReferenced() { + final JsonObject policyJson = buildPolicyJsonWithLocalRef(policyId, + subjectsJson(subject2), + resourcesJson("thing:/", List.of("READ"), List.of()), + JsonObject.empty(), + resourcesJson("thing:/", List.of("READ"), List.of()), + "shared-subjects"); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + // Try to delete shared-subjects while consumer references it + deletePolicyEntry(policyId, "shared-subjects") + .expectingHttpStatus(CONFLICT) + .fire(); + } + + @Test + public void deleteReferencedEntrySucceedsAfterRemovingReference() { + final JsonObject policyJson = buildPolicyJsonWithLocalRef(policyId, + subjectsJson(subject2), + resourcesJson("thing:/", List.of("READ"), List.of()), + JsonObject.empty(), + resourcesJson("thing:/", List.of("READ"), List.of()), + "shared-subjects"); + putPolicy(policyId, policyJson).expectingHttpStatus(CREATED).fire(); + + // Remove local reference first + deleteReferences(policyId, "consumer") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Now deleting the entry succeeds + deletePolicyEntry(policyId, "shared-subjects") + .expectingHttpStatus(NO_CONTENT) + .fire(); + } + + // ---- Helpers ---- + + private void putThingWithPolicy(final String thingId, final PolicyId policyId) { + putThing(TestConstants.API_V_2, JsonObject.newBuilder() + .set("thingId", thingId) + .set("policyId", policyId.toString()) + .build(), + org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) + .expectingHttpStatus(CREATED) + .fire(); + } + + private JsonObject buildPolicyJsonWithLocalRef(final PolicyId policyId, + final JsonObject sharedSubjects, final JsonObject sharedResources, + final JsonObject consumerSubjects, final JsonObject consumerResources, + final String referencedEntry) { + + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("shared-subjects", JsonObject.newBuilder() + .set("subjects", sharedSubjects) + .set("resources", sharedResources) + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", consumerSubjects) + .set("resources", consumerResources) + .set("references", JsonArray.of(localRef(referencedEntry))) + .build()) + .build()) + .build(); + } + + private JsonObject buildAdminEntryJson() { + return JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build(); + } + + private static JsonObject subjectsJson(final Subject subject) { + return JsonObject.newBuilder() + .set(subject.getId().toString(), subject.toJson()) + .build(); + } + + private static JsonObject resourcesJson(final String path, final List grant, + final List revoke) { + return JsonObject.newBuilder() + .set(path, JsonObject.newBuilder() + .set("grant", toJsonArray(grant)) + .set("revoke", toJsonArray(revoke)) + .build()) + .build(); + } + + private static JsonArray toJsonArray(final List strings) { + final var builder = JsonFactory.newArrayBuilder(); + strings.forEach(builder::add); + return builder.build(); + } + + private static JsonObject localRef(final String entryLabel) { + return JsonObject.newBuilder() + .set("entry", entryLabel) + .build(); + } + + private static DeleteMatcher deleteReferences(final PolicyId policyId, final String label) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "References"); + } + +} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryReferencesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryReferencesIT.java new file mode 100644 index 0000000..78746fd --- /dev/null +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyEntryReferencesIT.java @@ -0,0 +1,626 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.testing.system.things.rest; + +import static org.eclipse.ditto.base.model.common.HttpStatus.BAD_REQUEST; +import static org.eclipse.ditto.base.model.common.HttpStatus.CONFLICT; +import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; +import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; +import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; +import static org.eclipse.ditto.base.model.common.HttpStatus.OK; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; +import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; +import static org.eclipse.ditto.things.api.Permission.READ; +import static org.eclipse.ditto.things.api.Permission.WRITE; + +import java.util.List; +import java.util.Set; + +import org.eclipse.ditto.json.JsonArray; +import org.eclipse.ditto.json.JsonFactory; +import org.eclipse.ditto.json.JsonObject; +import org.eclipse.ditto.json.JsonPointer; +import org.eclipse.ditto.policies.model.EffectedImports; +import org.eclipse.ditto.policies.model.ImportableType; +import org.eclipse.ditto.policies.model.Label; +import org.eclipse.ditto.policies.model.PoliciesModelFactory; +import org.eclipse.ditto.policies.model.Policy; +import org.eclipse.ditto.policies.model.PolicyEntry; +import org.eclipse.ditto.policies.model.PolicyId; +import org.eclipse.ditto.policies.model.PolicyImport; +import org.eclipse.ditto.policies.model.Subject; +import org.eclipse.ditto.testing.common.IntegrationTest; +import org.eclipse.ditto.testing.common.ResourcePathBuilder; +import org.eclipse.ditto.testing.common.TestConstants; +import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; +import org.eclipse.ditto.testing.common.matcher.GetMatcher; +import org.eclipse.ditto.testing.common.matcher.PutMatcher; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for CRUD operations on the {@code /entries/{label}/references} sub-resource + * and referential integrity validation of policy entry references. + * + * @since 3.9.0 + */ +public final class PolicyEntryReferencesIT extends IntegrationTest { + + private PolicyId templatePolicyId; + private PolicyId importingPolicyId; + private Subject defaultSubject; + private Subject subject2; + + @Before + public void setUp() { + templatePolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("template")); + importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); + defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); + subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); + } + + // ---- CRUD tests ---- + + @Test + public void getReferencesOnEntryWithoutReferencesReturnsEmpty() { + final Policy policy = buildAdminOnlyPolicy(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + getReferences(importingPolicyId, "ADMIN") + .expectingBody(containsOnly(JsonArray.empty())) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void putImportReferenceAndRetrieveIt() { + createTemplateAndImportingPolicy(); + + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "user-access") + .expectingBody(containsOnly(refs)) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void putLocalReferenceAndRetrieveIt() { + // Policy with two entries: "shared-subjects" and "consumer" + final Policy policy = buildPolicyWithSharedSubjectsEntry(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + final JsonArray refs = JsonArray.of(localRef("shared-subjects")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "consumer") + .expectingBody(containsOnly(refs)) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void putMixedReferencesAndRetrieveThem() { + createTemplatePolicy(); + + // Build importing policy with "shared-subjects" + "consumer" entries and an import + final Policy policy = buildImportingPolicyWithSharedSubjects(importingPolicyId, templatePolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + final JsonArray refs = JsonArray.of( + importRef(templatePolicyId, "DEFAULT"), + localRef("shared-subjects") + ); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "consumer") + .expectingBody(containsOnly(refs)) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void deleteReferencesRemovesAll() { + createTemplateAndImportingPolicy(); + + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + deleteReferences(importingPolicyId, "user-access") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "user-access") + .expectingBody(containsOnly(JsonArray.empty())) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void putReferencesReplacesExisting() { + final Policy policy = buildPolicyWithSharedSubjectsEntry(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // First PUT with local ref + final JsonArray refs1 = JsonArray.of(localRef("shared-subjects")); + putReferences(importingPolicyId, "consumer", refs1) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Second PUT with empty array replaces + final JsonArray refs2 = JsonArray.empty(); + putReferences(importingPolicyId, "consumer", refs2) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "consumer") + .expectingBody(containsOnly(JsonArray.empty())) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void putEmptyArrayClearsReferences() { + createTemplateAndImportingPolicy(); + + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + putReferences(importingPolicyId, "user-access", JsonArray.empty()) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + getReferences(importingPolicyId, "user-access") + .expectingBody(containsOnly(JsonArray.empty())) + .expectingHttpStatus(OK) + .fire(); + } + + @Test + public void getReferencesOnNonExistentEntryReturns404() { + final Policy policy = buildAdminOnlyPolicy(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + getReferences(importingPolicyId, "NONEXISTENT") + .expectingHttpStatus(NOT_FOUND) + .fire(); + } + + @Test + public void createPolicyWithEntryReferencesAndRetrieve() { + createTemplatePolicy(); + + // Build importing policy JSON with references in the entry + final JsonObject policyJson = buildImportingPolicyJsonWithReferences( + importingPolicyId, templatePolicyId, + JsonArray.of(importRef(templatePolicyId, "DEFAULT")) + ); + putPolicy(importingPolicyId, policyJson) + .expectingHttpStatus(CREATED) + .fire(); + + getReferences(importingPolicyId, "user-access") + .expectingBody(containsOnly(JsonArray.of(importRef(templatePolicyId, "DEFAULT")))) + .expectingHttpStatus(OK) + .fire(); + } + + // ---- Referential integrity tests ---- + + @Test + public void localReferenceToNonExistentEntryReturns400() { + final Policy policy = buildAdminOnlyPolicy(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // Add a "consumer" entry first + final PolicyEntry consumerEntry = PoliciesModelFactory.newPolicyEntry("consumer", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, consumerEntry) + .expectingHttpStatus(CREATED) + .fire(); + + // Try to reference a non-existent entry + final JsonArray refs = JsonArray.of(localRef("NONEXISTENT")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(BAD_REQUEST) + .fire(); + } + + @Test + public void importReferenceToUndeclaredImportReturns400() { + // Policy with no imports + final Policy policy = buildAdminOnlyPolicy(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // Add a "consumer" entry + final PolicyEntry consumerEntry = PoliciesModelFactory.newPolicyEntry("consumer", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, consumerEntry) + .expectingHttpStatus(CREATED) + .fire(); + + // Try to reference an import that doesn't exist + final PolicyId nonImportedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("nonimported")); + final JsonArray refs = JsonArray.of(importRef(nonImportedPolicyId, "DEFAULT")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(BAD_REQUEST) + .fire(); + } + + @Test + public void importReferenceToImportableNeverEntryIsRejected() { + // Template with DEFAULT entry marked NEVER + final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry) + .set(defaultEntry) + .build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + + // Importing policy imports template + final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( + List.of(Label.of("DEFAULT"))); + final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templatePolicyId, effectedImports); + + final Policy importingPolicy = PoliciesModelFactory.newPolicyBuilder(importingPolicyId) + .forLabel("ADMIN") + .setSubject(defaultSubject) + .setGrantedPermissions(policyResource("/"), READ, WRITE) + .setGrantedPermissions(thingResource("/"), READ, WRITE) + .forLabel("user-access") + .setSubject(subject2) + .setGrantedPermissions(thingResource("/"), READ) + .setPolicyImports(PoliciesModelFactory.newPolicyImports(List.of(policyImport))) + .build(); + putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); + + // Try to add import reference to the NEVER entry + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(BAD_REQUEST) + .fire(); + } + + @Test + public void selfReferencingLocalEntryIsRejected() { + final Policy policy = buildAdminOnlyPolicy(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // Add a "consumer" entry + final PolicyEntry consumerEntry = PoliciesModelFactory.newPolicyEntry("consumer", + List.of(subject2), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.NEVER, Set.of()); + putPolicyEntry(importingPolicyId, consumerEntry) + .expectingHttpStatus(CREATED) + .fire(); + + // Try to self-reference + final JsonArray refs = JsonArray.of(localRef("consumer")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(BAD_REQUEST) + .fire(); + } + + @Test + public void deleteImportReferencedByEntryReturns409() { + createTemplateAndImportingPolicy(); + + // Add import reference + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Try to delete the import - should fail with 409 + final String importPath = ResourcePathBuilder.forPolicy(importingPolicyId) + .policyImport(templatePolicyId).toString(); + delete(dittoUrl(TestConstants.API_V_2, importPath)) + .expectingHttpStatus(CONFLICT) + .fire(); + } + + @Test + public void deleteImportSucceedsAfterRemovingReferences() { + createTemplateAndImportingPolicy(); + + // Add import reference + final JsonArray refs = JsonArray.of(importRef(templatePolicyId, "DEFAULT")); + putReferences(importingPolicyId, "user-access", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Remove references first + deleteReferences(importingPolicyId, "user-access") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Now deleting the import should succeed + final String importPath = ResourcePathBuilder.forPolicy(importingPolicyId) + .policyImport(templatePolicyId).toString(); + delete(dittoUrl(TestConstants.API_V_2, importPath)) + .expectingHttpStatus(NO_CONTENT) + .fire(); + } + + @Test + public void deleteEntryReferencedByLocalReferenceReturns409() { + final Policy policy = buildPolicyWithSharedSubjectsEntry(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // Add local reference from consumer to shared-subjects + final JsonArray refs = JsonArray.of(localRef("shared-subjects")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Try to delete shared-subjects - should fail with 409 + deletePolicyEntry(importingPolicyId, "shared-subjects") + .expectingHttpStatus(CONFLICT) + .fire(); + } + + @Test + public void deleteEntrySucceedsAfterRemovingLocalReferences() { + final Policy policy = buildPolicyWithSharedSubjectsEntry(importingPolicyId); + putPolicy(policy).expectingHttpStatus(CREATED).fire(); + + // Add local reference + final JsonArray refs = JsonArray.of(localRef("shared-subjects")); + putReferences(importingPolicyId, "consumer", refs) + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Remove local reference first + deleteReferences(importingPolicyId, "consumer") + .expectingHttpStatus(NO_CONTENT) + .fire(); + + // Now deleting the entry should succeed + deletePolicyEntry(importingPolicyId, "shared-subjects") + .expectingHttpStatus(NO_CONTENT) + .fire(); + } + + @Test + public void createPolicyWithBrokenLocalReferenceReturns400() { + // Build policy JSON with entry A referencing non-existent entry B + final JsonObject policyJson = JsonObject.newBuilder() + .set("policyId", importingPolicyId.toString()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(defaultSubject.getId().toString(), defaultSubject.toJson()) + .build()) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build()) + .set("consumer", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(subject2.getId().toString(), subject2.toJson()) + .build()) + .set("resources", JsonObject.newBuilder() + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder().add("READ").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("references", JsonArray.of(localRef("NONEXISTENT"))) + .build()) + .build()) + .build(); + + putPolicy(importingPolicyId, policyJson) + .expectingHttpStatus(BAD_REQUEST) + .fire(); + } + + // ---- Helpers ---- + + private void createTemplatePolicy() { + final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(policyResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), + ImportableType.NEVER, Set.of()); + final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", + List.of(defaultSubject), + List.of(PoliciesModelFactory.newResource(thingResource("/"), + PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), + ImportableType.IMPLICIT, Set.of()); + final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) + .set(adminEntry) + .set(defaultEntry) + .build(); + putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); + } + + private void createTemplateAndImportingPolicy() { + createTemplatePolicy(); + + final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( + List.of(Label.of("DEFAULT"))); + final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templatePolicyId, effectedImports); + + final Policy importingPolicy = PoliciesModelFactory.newPolicyBuilder(importingPolicyId) + .forLabel("ADMIN") + .setSubject(defaultSubject) + .setGrantedPermissions(policyResource("/"), READ, WRITE) + .setGrantedPermissions(thingResource("/"), READ, WRITE) + .forLabel("user-access") + .setSubject(subject2) + .setGrantedPermissions(thingResource("/"), READ) + .setPolicyImports(PoliciesModelFactory.newPolicyImports(List.of(policyImport))) + .build(); + putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); + } + + private Policy buildAdminOnlyPolicy(final PolicyId policyId) { + return PoliciesModelFactory.newPolicyBuilder(policyId) + .forLabel("ADMIN") + .setSubject(defaultSubject) + .setGrantedPermissions(policyResource("/"), READ, WRITE) + .setGrantedPermissions(thingResource("/"), READ, WRITE) + .build(); + } + + private Policy buildPolicyWithSharedSubjectsEntry(final PolicyId policyId) { + return PoliciesModelFactory.newPolicyBuilder(policyId) + .forLabel("ADMIN") + .setSubject(defaultSubject) + .setGrantedPermissions(policyResource("/"), READ, WRITE) + .setGrantedPermissions(thingResource("/"), READ, WRITE) + .forLabel("shared-subjects") + .setSubject(subject2) + .setGrantedPermissions(thingResource("/"), READ) + .forLabel("consumer") + .setSubject(defaultSubject) + .setGrantedPermissions(thingResource("/"), READ) + .build(); + } + + private Policy buildImportingPolicyWithSharedSubjects(final PolicyId policyId, + final PolicyId templateId) { + + final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( + List.of(Label.of("DEFAULT"))); + final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templateId, effectedImports); + + return PoliciesModelFactory.newPolicyBuilder(policyId) + .forLabel("ADMIN") + .setSubject(defaultSubject) + .setGrantedPermissions(policyResource("/"), READ, WRITE) + .setGrantedPermissions(thingResource("/"), READ, WRITE) + .forLabel("shared-subjects") + .setSubject(subject2) + .setGrantedPermissions(thingResource("/"), READ) + .forLabel("consumer") + .setSubject(defaultSubject) + .setGrantedPermissions(thingResource("/"), READ) + .setPolicyImports(PoliciesModelFactory.newPolicyImports(List.of(policyImport))) + .build(); + } + + private JsonObject buildImportingPolicyJsonWithReferences(final PolicyId policyId, + final PolicyId templateId, final JsonArray references) { + + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(defaultSubject.getId().toString(), defaultSubject.toJson()) + .build()) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", JsonObject.newBuilder() + .set(subject2.getId().toString(), subject2.toJson()) + .build()) + .set("resources", JsonObject.newBuilder() + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder().add("READ").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("references", references) + .build()) + .build()) + .build(); + } + + private static JsonObject importRef(final PolicyId policyId, final String entryLabel) { + return JsonObject.newBuilder() + .set("import", policyId.toString()) + .set("entry", entryLabel) + .build(); + } + + private static JsonObject localRef(final String entryLabel) { + return JsonObject.newBuilder() + .set("entry", entryLabel) + .build(); + } + + private static GetMatcher getReferences(final PolicyId policyId, final String label) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return get(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "References"); + } + + private static PutMatcher putReferences(final PolicyId policyId, final String label, + final JsonArray references) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return put(dittoUrl(TestConstants.API_V_2, path), references.toString()) + .withLogging(LOGGER, "References"); + } + + private static DeleteMatcher deleteReferences(final PolicyId policyId, final String label) { + final String path = ResourcePathBuilder.forPolicy(policyId) + .policyEntryReferences(label).toString(); + return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "References"); + } + +} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportEntriesAdditionsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportEntriesAdditionsIT.java deleted file mode 100644 index 2924f14..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportEntriesAdditionsIT.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.rest; - -import static org.eclipse.ditto.base.model.common.HttpStatus.BAD_REQUEST; -import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; -import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; -import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; -import static org.eclipse.ditto.base.model.common.HttpStatus.OK; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.List; -import java.util.Set; - -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyEntry; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Resource; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.TestConstants; -import org.junit.Before; -import org.junit.Test; - -/** - * Integration tests for policy import {@code entriesAdditions} and {@code allowedImportAdditions} features. - */ -public final class PolicyImportEntriesAdditionsIT extends IntegrationTest { - - private PolicyId importedPolicyId; - private PolicyId importingPolicyId; - private Subject defaultSubject; - private Subject subject2; - - @Before - public void setUp() { - importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - } - - @Test - public void putPolicyImportWithSubjectAdditionsAllowed() { - // Template allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy with entriesAdditions adding subject2 - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Verify the import was created - getPolicy(importingPolicyId) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putPolicyImportWithResourceAdditionsAllowed() { - // Template allows resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy with entriesAdditions adding a resource - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), null, - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - getPolicy(importingPolicyId) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putPolicyImportWithSubjectAndResourceAdditionsAllowed() { - // Template allows both subject and resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy with entriesAdditions adding both subject and resource - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - getPolicy(importingPolicyId) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putPolicyImportWithSubjectAdditionsDisallowedIsRejected() { - // Template does NOT allow any additions (no allowedImportAdditions set) - final Policy importedPolicy = buildImportedPolicyWithoutAllowedAdditions(importedPolicyId); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy tries to add subjects via entriesAdditions - should be rejected - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - - putPolicy(importingPolicy) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); - } - - @Test - public void putPolicyImportWithResourceAdditionsWhenOnlySubjectsAllowed() { - // Template allows only subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy tries to add resources - should be rejected - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), null, - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - - putPolicy(importingPolicy) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); - } - - @Test - public void putPolicyImportWithAdditionsForEntryNotInEntriesArrayFails() { - // Template with allowedImportAdditions on DEFAULT entry - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // entriesAdditions references a label "NON_EXISTENT" not in the entries array - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("NON_EXISTENT"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - - putPolicy(importingPolicy) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); - } - - @Test - public void thingAccessibleViaSubjectAddedThroughEntriesAdditions() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy with subject2 added via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 can access the thing via the imported subject - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void thingNotAccessibleWhenSubjectAdditionsNotAllowed() { - // Template does NOT allow additions - final Policy importedPolicy = buildImportedPolicyWithoutAllowedAdditions(importedPolicyId); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Creating an importing policy with subject additions should be rejected - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - - putPolicy(importingPolicy) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); - - // Create a simple importing policy without additions - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy simpleImportingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(simpleImportingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 cannot access the thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void putPolicyImportWithMultipleResourceAdditionsAllowed() { - // Template allows resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy with entriesAdditions adding multiple resources in a single addition - final Resource attrResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of())); - final Resource featResource = PoliciesModelFactory.newResource(thingResource("/features"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), null, - PoliciesModelFactory.newResources(attrResource, featResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - getPolicy(importingPolicyId) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putPolicyImportWithAdditionsForMultipleLabels() { - // Template with DEFAULT and EXTRA entries, both allow subject additions - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - final PolicyEntry extraEntry = PoliciesModelFactory.newPolicyEntry("EXTRA", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.EXPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .set(extraEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy with entriesAdditions targeting both labels - final EntryAddition defaultAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntryAddition extraAddition = PoliciesModelFactory.newEntryAddition( - Label.of("EXTRA"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions( - List.of(defaultAddition, extraAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT"), Label.of("EXTRA")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - getPolicy(importingPolicyId) - .expectingHttpStatus(OK) - .fire(); - } - - /** - * Builds an imported (template) policy with a DEFAULT entry that grants thing:/ READ - * and has the specified {@code allowedImportAdditions}. - */ - private Policy buildImportedPolicy(final PolicyId policyId, - final Set allowedImportAdditions) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - /** - * Builds an imported (template) policy without any {@code allowedImportAdditions}. - */ - private Policy buildImportedPolicyWithoutAllowedAdditions(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel("DEFAULT") - .setSubject(defaultSubject) - .setGrantedPermissions(thingResource("/"), READ) - .build(); - } - - /** - * Builds a basic importing policy with an ADMIN entry (full access on policy:/ and thing:/). - */ - private Policy buildImportingPolicy(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .build(); - } - - /** - * Builds an importing policy that imports from the given imported policy and adds the given subject - * via {@code entriesAdditions}. - */ - private Policy buildImportingPolicyWithSubjectAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportSubResourcesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportSubResourcesIT.java deleted file mode 100644 index 7f0d051..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportSubResourcesIT.java +++ /dev/null @@ -1,481 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.rest; - -import static org.eclipse.ditto.base.model.common.HttpStatus.BAD_REQUEST; -import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; -import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; -import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; -import static org.eclipse.ditto.base.model.common.HttpStatus.OK; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.List; -import java.util.Set; - -import org.eclipse.ditto.json.JsonArray; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyEntry; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Resource; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.ResourcePathBuilder; -import org.eclipse.ditto.testing.common.TestConstants; -import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; -import org.eclipse.ditto.testing.common.matcher.GetMatcher; -import org.eclipse.ditto.testing.common.matcher.PutMatcher; -import org.junit.Before; -import org.junit.Test; - -/** - * Integration tests for the dedicated policy import sub-resource HTTP routes: - *
    - *
  • GET/PUT {@code /imports/{id}/entries}
  • - *
  • GET/PUT {@code /imports/{id}/entriesAdditions}
  • - *
  • GET/PUT/DELETE {@code /imports/{id}/entriesAdditions/{label}}
  • - *
- */ -public final class PolicyImportSubResourcesIT extends IntegrationTest { - - private PolicyId importedPolicyId; - private PolicyId importingPolicyId; - private Subject defaultSubject; - private Subject subject2; - - @Before - public void setUp() { - importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - } - - @Test - public void getAndPutPolicyImportEntries() { - // Create imported policy with two importable entries: DEFAULT and EXTRA - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, Set.of()); - final PolicyEntry extraEntry = PoliciesModelFactory.newPolicyEntry("EXTRA", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.EXPLICIT, Set.of()); - - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .set(extraEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy that imports only DEFAULT - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // GET entries and verify only DEFAULT is listed - final JsonArray expectedEntries = JsonArray.newBuilder().add("DEFAULT").build(); - getPolicyImportEntries(importingPolicyId, importedPolicyId) - .expectingBody(containsOnly(expectedEntries)) - .expectingHttpStatus(OK) - .fire(); - - // PUT entries to also import EXTRA - final JsonArray updatedEntries = JsonArray.newBuilder().add("DEFAULT").add("EXTRA").build(); - putPolicyImportEntries(importingPolicyId, importedPolicyId, updatedEntries) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // GET entries again and verify both are listed - getPolicyImportEntries(importingPolicyId, importedPolicyId) - .expectingBody(containsOnly(updatedEntries)) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void getAndPutPolicyImportEntriesAdditions() { - // Create imported policy with DEFAULT entry that allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy with subject2 added via entriesAdditions - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(policyImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // GET entriesAdditions and verify - getPolicyImportEntriesAdditions(importingPolicyId, importedPolicyId) - .expectingBody(containsOnly(additions.toJson())) - .expectingHttpStatus(OK) - .fire(); - - // PUT empty entriesAdditions - putPolicyImportEntriesAdditions(importingPolicyId, importedPolicyId, JsonObject.empty()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // GET entriesAdditions and verify empty - getPolicyImportEntriesAdditions(importingPolicyId, importedPolicyId) - .expectingBody(containsOnly(JsonObject.empty())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putGetAndDeletePolicyImportEntryAddition() { - // Create imported policy with DEFAULT entry that allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy without additions - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // PUT single entry addition for DEFAULT - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), null); - final String additionBody = entryAdditionBodyString(Label.of("DEFAULT"), entryAddition); - putPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT", additionBody) - .expectingHttpStatus(CREATED) - .fire(); - - // GET single entry addition - getPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT") - .expectingHttpStatus(OK) - .fire(); - - // DELETE single entry addition - deletePolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT") - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // GET after delete returns 404 - getPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT") - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - @Test - public void getPolicyImportEntryAdditionForNonExistentLabelFails() { - // Create imported policy - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy without additions - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // GET non-existent entry addition returns 404 - getPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "NONEXISTENT") - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - @Test - public void putPolicyImportEntryAdditionViaSubResourceGrantsAccess() { - // Create imported policy with DEFAULT entry that allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy with a simple import (no additions) - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 cannot access the thing initially - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Add subject2 via the entriesAdditions sub-resource route - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), null); - final String additionBody = entryAdditionBodyString(Label.of("DEFAULT"), entryAddition); - putPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT", additionBody) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 can now access the thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void deletePolicyImportEntryAdditionRemovesAccess() { - // Create imported policy with DEFAULT entry that allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy with subject2 added via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 can access the thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // DELETE the entry addition for DEFAULT - deletePolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT") - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Verify user2 can no longer access the thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void putPolicyImportEntryAdditionViaSubResourceBypassesAllowedAdditionsCheck() { - // Create imported policy that only allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy without additions - final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId) - .toBuilder().setPolicyImport(simpleImport).build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // PUT a resource addition via the sub-resource route — accepted because - // the sub-resource route does not validate against allowedImportAdditions - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of())); - final EntryAddition resourceAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), null, - PoliciesModelFactory.newResources(additionalResource)); - final String additionBody = entryAdditionBodyString(Label.of("DEFAULT"), resourceAddition); - putPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT", additionBody) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify the addition was stored - getPolicyImportEntryAddition(importingPolicyId, importedPolicyId, "DEFAULT") - .expectingHttpStatus(OK) - .fire(); - - // In contrast, modifying the full import with resource additions IS rejected - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(resourceAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport importWithResourceAdditions = - PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - putPolicyImport(importingPolicyId, importWithResourceAdditions) - .expectingHttpStatus(BAD_REQUEST) - .expectingErrorCode("policies:import.invalid") - .fire(); - } - - // --- Helper methods for building policies --- - - private Policy buildImportedPolicy(final PolicyId policyId, - final Set allowedImportAdditions) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - private Policy buildImportingPolicy(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .build(); - } - - private Policy buildImportingPolicyWithSubjectAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - - /** - * Extracts the JSON body for a single entry addition by label from an EntriesAdditions object. - */ - private static String entryAdditionBodyString(final Label label, final EntryAddition entryAddition) { - return PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)) - .toJson() - .getValue(label.toString()) - .map(JsonValue::toString) - .orElseThrow(); - } - - // --- Helper methods for sub-resource HTTP operations --- - - private static GetMatcher getPolicyImportEntries(final CharSequence policyId, - final CharSequence importedPolicyId) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entries"; - return get(dittoUrl(TestConstants.API_V_2, path)) - .withLogging(LOGGER, "PolicyImportEntries"); - } - - private static PutMatcher putPolicyImportEntries(final CharSequence policyId, - final CharSequence importedPolicyId, final JsonArray entries) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entries"; - return put(dittoUrl(TestConstants.API_V_2, path), entries.toString()) - .withLogging(LOGGER, "PolicyImportEntries"); - } - - private static GetMatcher getPolicyImportEntriesAdditions(final CharSequence policyId, - final CharSequence importedPolicyId) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entriesAdditions"; - return get(dittoUrl(TestConstants.API_V_2, path)) - .withLogging(LOGGER, "PolicyImportEntriesAdditions"); - } - - private static PutMatcher putPolicyImportEntriesAdditions(final CharSequence policyId, - final CharSequence importedPolicyId, final JsonObject entriesAdditions) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entriesAdditions"; - return put(dittoUrl(TestConstants.API_V_2, path), entriesAdditions.toString()) - .withLogging(LOGGER, "PolicyImportEntriesAdditions"); - } - - private static GetMatcher getPolicyImportEntryAddition(final CharSequence policyId, - final CharSequence importedPolicyId, final CharSequence label) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entriesAdditions/" + label; - return get(dittoUrl(TestConstants.API_V_2, path)) - .withLogging(LOGGER, "PolicyImportEntryAddition"); - } - - private static PutMatcher putPolicyImportEntryAddition(final CharSequence policyId, - final CharSequence importedPolicyId, final CharSequence label, final String body) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entriesAdditions/" + label; - return put(dittoUrl(TestConstants.API_V_2, path), body) - .withLogging(LOGGER, "PolicyImportEntryAddition"); - } - - private static DeleteMatcher deletePolicyImportEntryAddition(final CharSequence policyId, - final CharSequence importedPolicyId, final CharSequence label) { - final String path = ResourcePathBuilder.forPolicy(policyId) - .policyImport(importedPolicyId).toString() + "/entriesAdditions/" + label; - return delete(dittoUrl(TestConstants.API_V_2, path)) - .withLogging(LOGGER, "PolicyImportEntryAddition"); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportTransitiveImportsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportTransitiveImportsIT.java index aabe9b0..d440f5b 100644 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportTransitiveImportsIT.java +++ b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportTransitiveImportsIT.java @@ -30,8 +30,6 @@ import org.eclipse.ditto.json.JsonObject; import org.eclipse.ditto.policies.model.AllowedImportAddition; import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; import org.eclipse.ditto.policies.model.ImportableType; import org.eclipse.ditto.policies.model.Label; import org.eclipse.ditto.policies.model.PoliciesModelFactory; @@ -90,7 +88,6 @@ public void putPolicyWithTransitiveImportsAndRetrieve() { // Leaf imports template with transitiveImports (even though single-level — tests serialization) final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( List.of(Label.of("DEFAULT")), - null, List.of(templateId)); final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templateId, effectedImports); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -114,13 +111,13 @@ public void getAndPutTransitiveImportsSubResource() { // Template putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - // Intermediate: imports template with entriesAdditions - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + // Intermediate: imports template, has user-access entry with import reference + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); // Leaf: imports intermediate without transitiveImports initially final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(intermediateId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() .setPolicyImport(simpleImport) .build(); @@ -223,7 +220,7 @@ public void twoLevelTransitiveImportGrantsThingAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -251,12 +248,12 @@ public void twoLevelImportWithoutTransitiveImportsDeniesAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); // Leaf imports intermediate WITHOUT transitiveImports final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(intermediateId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() .setPolicyImport(simpleImport) .build(); @@ -265,7 +262,7 @@ public void twoLevelImportWithoutTransitiveImportsDeniesAccess() { final String thingId = leafId.toString(); createThing(thingId, leafId); - // subject2 should NOT have access — intermediate has no inline entries + // subject2 should NOT have access — intermediate's import ref unresolved without transitiveImports getThing(TestConstants.API_V_2, thingId) .withConfiguredAuth(serviceEnv.getTestingContext2()) .expectingHttpStatus(NOT_FOUND) @@ -282,12 +279,12 @@ public void addingTransitiveImportsViaSubResourceGrantsAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); // Leaf imports intermediate WITHOUT transitiveImports final PolicyImport simpleImport = PoliciesModelFactory.newPolicyImport(intermediateId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() .setPolicyImport(simpleImport) .build(); @@ -326,7 +323,7 @@ public void removingTransitiveImportsRevokesAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -361,7 +358,7 @@ public void templateChangePropagatesToTransitiveConsumer() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -407,7 +404,7 @@ public void deletingTemplatePolicyRevokesTransitiveAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leafId, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -460,13 +457,12 @@ public void threeLevelTransitiveImportGrantsThingAccess() { putPolicy(buildTemplatePolicy(globalTemplateId)).expectingHttpStatus(CREATED).fire(); // C: regional — imports D with entriesAdditions adding subject2 - putPolicy(buildIntermediatePolicy(regionalId, globalTemplateId, subject2)) + putPolicy(regionalId, buildIntermediatePolicyJson(regionalId, globalTemplateId, subject2)) .expectingHttpStatus(CREATED).fire(); - // B: department — imports C with transitiveImports=["D"], no inline entries + // B: department — imports C with transitiveImports=["D"] final EffectedImports deptEffectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(globalTemplateId)); final PolicyImport deptImport = PoliciesModelFactory.newPolicyImport(regionalId, deptEffectedImports); final Policy departmentPolicy = buildAdminOnlyPolicy(departmentId).toBuilder() @@ -474,10 +470,9 @@ public void threeLevelTransitiveImportGrantsThingAccess() { .build(); putPolicy(departmentPolicy).expectingHttpStatus(CREATED).fire(); - // A: leaf — imports B with transitiveImports=["C"], entries=["DEFAULT"] + // A: leaf — imports B with transitiveImports=["C"], entries=["user-access"] final EffectedImports leafEffectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(regionalId)); final PolicyImport leafImport = PoliciesModelFactory.newPolicyImport(departmentId, leafEffectedImports); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -509,12 +504,12 @@ public void threeLevelTransitiveImportBrokenChainDeniesAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(globalTemplateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(regionalId, globalTemplateId, subject2)) + putPolicy(regionalId, buildIntermediatePolicyJson(regionalId, globalTemplateId, subject2)) .expectingHttpStatus(CREATED).fire(); // B: department — imports C WITHOUT transitiveImports (chain break) final PolicyImport deptImport = PoliciesModelFactory.newPolicyImport(regionalId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy departmentPolicy = buildAdminOnlyPolicy(departmentId).toBuilder() .setPolicyImport(deptImport) .build(); @@ -522,8 +517,7 @@ public void threeLevelTransitiveImportBrokenChainDeniesAccess() { // A: leaf — imports B with transitiveImports=["C"] final EffectedImports leafEffectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(regionalId)); final PolicyImport leafImport = PoliciesModelFactory.newPolicyImport(departmentId, leafEffectedImports); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -552,12 +546,12 @@ public void fixingBrokenChainByAddingTransitiveImportsGrantsAccess() { final PolicyId leafId = PolicyId.of(idGenerator().withPrefixedRandomName("leaf")); putPolicy(buildTemplatePolicy(globalTemplateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(regionalId, globalTemplateId, subject2)) + putPolicy(regionalId, buildIntermediatePolicyJson(regionalId, globalTemplateId, subject2)) .expectingHttpStatus(CREATED).fire(); // B: department — imports C WITHOUT transitiveImports initially final PolicyImport deptImport = PoliciesModelFactory.newPolicyImport(regionalId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); + PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("user-access")))); final Policy departmentPolicy = buildAdminOnlyPolicy(departmentId).toBuilder() .setPolicyImport(deptImport) .build(); @@ -565,8 +559,7 @@ public void fixingBrokenChainByAddingTransitiveImportsGrantsAccess() { // A: leaf — imports B with transitiveImports=["C"] final EffectedImports leafEffectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(regionalId)); final PolicyImport leafImport = PoliciesModelFactory.newPolicyImport(departmentId, leafEffectedImports); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -633,26 +626,36 @@ public void threeLevelTransitiveWithMultipleEntryLabels() { .build(); putPolicy(templatePolicy).expectingHttpStatus(CREATED).fire(); - // C: regional — imports D, adds subject2 to both READER and WRITER - final EntryAddition readerAddition = PoliciesModelFactory.newEntryAddition( - Label.of("READER"), PoliciesModelFactory.newSubjects(subject2), null); - final EntryAddition writerAddition = PoliciesModelFactory.newEntryAddition( - Label.of("WRITER"), PoliciesModelFactory.newSubjects(subject2), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions( - List.of(readerAddition, writerAddition)); - final EffectedImports regionalEffected = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("READER"), Label.of("WRITER")), additions); - final PolicyImport regionalImport = PoliciesModelFactory.newPolicyImport( - globalTemplateId, regionalEffected); - final Policy regionalPolicy = buildAdminOnlyPolicy(regionalId).toBuilder() - .setPolicyImport(regionalImport) + // C: regional — imports D, has two entries with import references + subject2 + final JsonObject regionalJson = JsonObject.newBuilder() + .set("policyId", regionalId.toString()) + .set("imports", JsonObject.newBuilder() + .set(globalTemplateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("READER").add("WRITER").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("reader-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of(importRef(globalTemplateId, "READER"))) + .set("importable", "implicit") + .build()) + .set("writer-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(subject2)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of(importRef(globalTemplateId, "WRITER"))) + .set("importable", "implicit") + .build()) + .build()) .build(); - putPolicy(regionalPolicy).expectingHttpStatus(CREATED).fire(); + putPolicy(regionalId, regionalJson).expectingHttpStatus(CREATED).fire(); // B: department — imports C with transitiveImports=["D"] final EffectedImports deptEffected = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("READER"), Label.of("WRITER")), - null, + List.of(Label.of("reader-access"), Label.of("writer-access")), List.of(globalTemplateId)); final PolicyImport deptImport = PoliciesModelFactory.newPolicyImport(regionalId, deptEffected); final Policy departmentPolicy = buildAdminOnlyPolicy(departmentId).toBuilder() @@ -662,8 +665,7 @@ public void threeLevelTransitiveWithMultipleEntryLabels() { // A: leaf — imports B with transitiveImports=["C"] final EffectedImports leafEffected = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("READER"), Label.of("WRITER")), - null, + List.of(Label.of("reader-access"), Label.of("writer-access")), List.of(regionalId)); final PolicyImport leafImport = PoliciesModelFactory.newPolicyImport(departmentId, leafEffected); final Policy leafPolicy = buildAdminOnlyPolicy(leafId).toBuilder() @@ -705,7 +707,7 @@ public void multipleConsumersOfSameTransitiveChainAreIndependent() { final PolicyId leaf2Id = PolicyId.of(idGenerator().withPrefixedRandomName("leaf2")); putPolicy(buildTemplatePolicy(templateId)).expectingHttpStatus(CREATED).fire(); - putPolicy(buildIntermediatePolicy(intermediateId, templateId, subject2)) + putPolicy(intermediateId, buildIntermediatePolicyJson(intermediateId, templateId, subject2)) .expectingHttpStatus(CREATED).fire(); putPolicy(buildLeafPolicyWithTransitiveImports(leaf1Id, intermediateId, templateId)) .expectingHttpStatus(CREATED).fire(); @@ -775,33 +777,41 @@ private Policy buildTemplatePolicy(final PolicyId policyId) { } /** - * Builds an intermediate policy that imports the template and adds the given subject - * via {@code entriesAdditions}. Has no inline thing entries of its own. + * Builds an intermediate policy that imports the template and has a "user-access" entry + * with the given subject and an import reference to the template's DEFAULT entry. + * The "user-access" entry is marked {@code importable=IMPLICIT} so downstream importers + * can include it. */ - private Policy buildIntermediatePolicy(final PolicyId policyId, final PolicyId templateId, + private JsonObject buildIntermediatePolicyJson(final PolicyId policyId, final PolicyId templateId, final Subject additionalSubject) { - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templateId, effectedImports); - - return buildAdminOnlyPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) + return JsonObject.newBuilder() + .set("policyId", policyId.toString()) + .set("imports", JsonObject.newBuilder() + .set(templateId.toString(), JsonObject.newBuilder() + .set("entries", JsonFactory.newArrayBuilder() + .add("DEFAULT").build()) + .build()) + .build()) + .set("entries", JsonObject.newBuilder() + .set("ADMIN", buildAdminEntryJson()) + .set("user-access", JsonObject.newBuilder() + .set("subjects", subjectsJson(additionalSubject)) + .set("resources", JsonObject.empty()) + .set("references", JsonArray.of(importRef(templateId, "DEFAULT"))) + .set("importable", "implicit") + .build()) + .build()) .build(); } /** * Builds a leaf policy that imports the intermediate with {@code transitiveImports} - * pointing to the template, requesting entry label "DEFAULT". + * pointing to the template, requesting entry label "user-access". */ private Policy buildLeafPolicyWithTransitiveImports(final PolicyId leafId, final PolicyId intermediateId, final PolicyId templateId) { final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), - null, + List.of(Label.of("user-access")), List.of(templateId)); final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(intermediateId, effectedImports); return buildAdminOnlyPolicy(leafId).toBuilder() @@ -852,4 +862,36 @@ private static PutMatcher putTransitiveImports(final CharSequence policyId, .withLogging(LOGGER, "TransitiveImports"); } + private JsonObject buildAdminEntryJson() { + return JsonObject.newBuilder() + .set("subjects", subjectsJson(defaultSubject)) + .set("resources", JsonObject.newBuilder() + .set("policy:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .set("thing:/", JsonObject.newBuilder() + .set("grant", JsonFactory.newArrayBuilder() + .add("READ").add("WRITE").build()) + .set("revoke", JsonArray.empty()) + .build()) + .build()) + .set("importable", "never") + .build(); + } + + private static JsonObject subjectsJson(final Subject subject) { + return JsonObject.newBuilder() + .set(subject.getId().toString(), subject.toJson()) + .build(); + } + + private static JsonObject importRef(final PolicyId policyId, final String entryLabel) { + return JsonObject.newBuilder() + .set("import", policyId.toString()) + .set("entry", entryLabel) + .build(); + } + } diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportsAliasesIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportsAliasesIT.java deleted file mode 100644 index 68a5d40..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/PolicyImportsAliasesIT.java +++ /dev/null @@ -1,610 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.rest; - -import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; -import static org.eclipse.ditto.base.model.common.HttpStatus.FORBIDDEN; -import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; -import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; -import static org.eclipse.ditto.base.model.common.HttpStatus.OK; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import org.eclipse.ditto.base.model.common.HttpStatus; -import org.eclipse.ditto.json.JsonFactory; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.json.JsonPointer; -import org.eclipse.ditto.json.JsonValue; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportsAlias; -import org.eclipse.ditto.policies.model.ImportsAliases; -import org.eclipse.ditto.policies.model.ImportsAliasTarget; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.policies.model.Subjects; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.ResourcePathBuilder; -import org.eclipse.ditto.testing.common.TestConstants; -import org.eclipse.ditto.testing.common.matcher.DeleteMatcher; -import org.eclipse.ditto.testing.common.matcher.GetMatcher; -import org.eclipse.ditto.testing.common.matcher.PutMatcher; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.things.model.ThingsModelFactory; -import org.junit.Before; -import org.junit.Test; - -/** - * Integration tests for {@code /policies//importsAliases} resources and subject fan-out through alias labels. - */ -public final class PolicyImportsAliasesIT extends IntegrationTest { - - private static final Label ALIAS_LABEL = Label.of("operator"); - private static final Label TARGET_LABEL_1 = Label.of("operator-reactor"); - private static final Label TARGET_LABEL_2 = Label.of("operator-turbine"); - - private PolicyId templatePolicyId; - private PolicyId importingPolicyId; - private Policy templatePolicy; - private Policy importingPolicy; - private ImportsAlias alias; - private ImportsAliases aliases; - - @Before - public void setUp() { - templatePolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("template")); - importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - - // Template policy: provides entries that can be imported with entriesAdditions - templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel("operator-reactor") - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor("operator-reactor", Set.of(AllowedImportAddition.SUBJECTS)) - .forLabel("operator-turbine") - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor("operator-turbine", Set.of(AllowedImportAddition.SUBJECTS)) - .build(); - - // Build the alias and its targets - final List targets = Arrays.asList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1), - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_2) - ); - alias = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, targets); - aliases = PoliciesModelFactory.newImportsAliases(Collections.singletonList(alias)); - - // Build the import with entriesAdditions for both target entries - final EntryAddition addition1 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_1, null, null); - final EntryAddition addition2 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_2, null, null); - final EntriesAdditions entriesAdditions = - PoliciesModelFactory.newEntriesAdditions(Arrays.asList(addition1, addition2)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - Arrays.asList(TARGET_LABEL_1, TARGET_LABEL_2), entriesAdditions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templatePolicyId, effectedImports); - - // Importing policy: has admin entry, imports template, defines alias - importingPolicy = PoliciesModelFactory.newPolicyBuilder(importingPolicyId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setPolicyImports(PoliciesModelFactory.newPolicyImports(Collections.singletonList(policyImport))) - .setImportsAliases(aliases) - .build(); - } - - // --- CRUD on /importsAliases --- - - @Test - public void createPolicyWithImportsAliasesAndRetrieveThem() { - createTemplateThenImportingPolicy(); - - getImportsAliases(importingPolicyId) - .expectingBody(containsOnly(aliases.toJson())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void retrieveSingleImportsAlias() { - createTemplateThenImportingPolicy(); - - getImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingBody(containsOnly(alias.toJson())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putAndGetSingleImportsAlias() { - // Create policies without alias first - putPolicy(templatePolicyId, templatePolicy).expectingHttpStatus(CREATED).fire(); - final Policy policyWithoutAlias = importingPolicy.toBuilder() - .setImportsAliases(PoliciesModelFactory.emptyImportsAliases()) - .build(); - putPolicy(importingPolicyId, policyWithoutAlias).expectingHttpStatus(CREATED).fire(); - - // PUT a single alias - putImportsAlias(importingPolicyId, ALIAS_LABEL, alias.toJson()) - .expectingHttpStatus(CREATED) - .fire(); - - // GET it back - getImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingBody(containsOnly(alias.toJson())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void modifyExistingImportsAlias() { - createTemplateThenImportingPolicy(); - - // Modify alias to only have one target - final ImportsAlias modifiedAlias = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, - Collections.singletonList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1))); - - putImportsAlias(importingPolicyId, ALIAS_LABEL, modifiedAlias.toJson()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - getImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingBody(containsOnly(modifiedAlias.toJson())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void deleteSingleImportsAlias() { - createTemplateThenImportingPolicy(); - - deleteImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - getImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - @Test - public void deleteAllImportsAliases() { - createTemplateThenImportingPolicy(); - - deleteImportsAliases(importingPolicyId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - getImportsAliases(importingPolicyId) - .expectingBody(containsOnly(JsonObject.empty())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putAllImportsAliases() { - createTemplateThenImportingPolicy(); - - // Replace all aliases with a different alias - final Label newAliasLabel = Label.of("inspector"); - final ImportsAlias newAlias = PoliciesModelFactory.newImportsAlias(newAliasLabel, - Collections.singletonList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1))); - final ImportsAliases newAliases = PoliciesModelFactory.newImportsAliases(Collections.singletonList(newAlias)); - - putImportsAliases(importingPolicyId, newAliases) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - getImportsAliases(importingPolicyId) - .expectingBody(containsOnly(newAliases.toJson())) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void getNonExistentImportsAliasReturns404() { - createTemplateThenImportingPolicy(); - - // Delete the alias first - deleteImportsAlias(importingPolicyId, ALIAS_LABEL).expectingHttpStatus(NO_CONTENT).fire(); - - getImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - // --- Subject fan-out through alias --- - - @Test - public void putSubjectsThroughAliasFansOutToAllTargets() { - createTemplateThenImportingPolicy(); - - final Subject newSubject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - final Subjects subjects = Subjects.newInstance(newSubject); - - // PUT subjects via the alias label (uses the entries/{label}/subjects endpoint) - putPolicyEntrySubjects(importingPolicyId, ALIAS_LABEL.toString(), subjects) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Verify subject was added — retrieving via alias returns subjects from first target - getPolicyEntrySubjects(importingPolicyId, ALIAS_LABEL.toString()) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void putSingleSubjectThroughAlias() { - createTemplateThenImportingPolicy(); - - final Subject newSubject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - final String subjectId = newSubject.getId().toString(); - - putPolicyEntrySubject(importingPolicyId, ALIAS_LABEL.toString(), subjectId, newSubject) - .expectingHttpStatus(CREATED) - .fire(); - - getPolicyEntrySubject(importingPolicyId, ALIAS_LABEL.toString(), subjectId) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void deleteSingleSubjectThroughAlias() { - createTemplateThenImportingPolicy(); - - final Subject newSubject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - final String subjectId = newSubject.getId().toString(); - - // First add a subject - putPolicyEntrySubject(importingPolicyId, ALIAS_LABEL.toString(), subjectId, newSubject) - .expectingHttpStatus(CREATED) - .fire(); - - // Then delete it via alias - deletePolicyEntrySubject(importingPolicyId, ALIAS_LABEL.toString(), subjectId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Verify it's gone - getPolicyEntrySubject(importingPolicyId, ALIAS_LABEL.toString(), subjectId) - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - @Test - public void retrieveSubjectsThroughAlias() { - createTemplateThenImportingPolicy(); - - getPolicyEntrySubjects(importingPolicyId, ALIAS_LABEL.toString()) - .expectingHttpStatus(OK) - .fire(); - } - - // --- Conflict and protection scenarios --- - - @Test - public void aliasLabelConflictsWithLocalEntry() { - putPolicy(templatePolicyId, templatePolicy).expectingHttpStatus(CREATED).fire(); - - // Build conflicting policy as raw JSON to bypass client-side model validation - // (the PolicyBuilder throws ImportsAliasConflictException locally) - final JsonObject operatorEntry = JsonObject.newBuilder() - .set("subjects", JsonObject.newBuilder() - .set(defaultSubject().getId().toString(), defaultSubject().toJson()) - .build()) - .set("resources", JsonObject.newBuilder() - .set("thing:/", JsonObject.newBuilder() - .set("grant", JsonFactory.newArrayBuilder().add("READ").build()) - .set("revoke", JsonFactory.newArray()) - .build()) - .build()) - .build(); - final JsonObject conflictingPolicyJson = importingPolicy.toJson().toBuilder() - .set(JsonPointer.of("entries/operator"), operatorEntry) - .build(); - - putPolicy(importingPolicyId, conflictingPolicyJson) - .expectingHttpStatus(HttpStatus.CONFLICT) - .fire(); - } - - @Test - public void deleteImportReferencedByAliasIsRejected() { - createTemplateThenImportingPolicy(); - - // Server returns 403 (policies:import.notmodifiable) when alias references the import - deletePolicyImport(importingPolicyId, templatePolicyId) - .expectingHttpStatus(FORBIDDEN) - .fire(); - } - - @Test - public void deleteImportSucceedsAfterRemovingAlias() { - createTemplateThenImportingPolicy(); - - // First remove the alias - deleteImportsAlias(importingPolicyId, ALIAS_LABEL) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Now deleting the import should succeed - deletePolicyImport(importingPolicyId, templatePolicyId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void resourceOperationsOnAliasLabelAreRejected() { - createTemplateThenImportingPolicy(); - - // Alias labels are not real entries — server returns 404 for non-subject operations - getPolicyEntryResources(importingPolicyId, ALIAS_LABEL.toString()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - // --- Policy enforcement through alias --- - - @Test - public void subjectAddedViaAliasGainsAccessToThing() { - final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("aliasEnforcement")); - - // Create template and importing policy separately (putThingWithPolicy strips imports/aliases) - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - final Policy tmplPolicy = buildTemplatePolicy(tmplPolicyId); - putPolicy(tmplPolicy).expectingHttpStatus(CREATED).fire(); - - final PolicyId thingPolicyId = PolicyId.of(thingId); - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create the Thing referencing the existing policy - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonPointer.of("status"), JsonValue.of("active")) - .build(); - putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 cannot access the Thing yet - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Add user2 as subject through the alias - final Subject user2Subject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - putPolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString(), user2Subject) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can now access the Thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - } - - @Test - public void subjectRemovedViaAliasLosesAccessToThing() { - final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("aliasRevoke")); - - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - final Policy tmplPolicy = buildTemplatePolicy(tmplPolicyId); - putPolicy(tmplPolicy).expectingHttpStatus(CREATED).fire(); - - final PolicyId thingPolicyId = PolicyId.of(thingId); - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).expectingHttpStatus(CREATED).fire(); - - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonPointer.of("status"), JsonValue.of("active")) - .build(); - putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Add user2 via alias - final Subject user2Subject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - putPolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString(), user2Subject) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 has access - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Remove user2 via alias - deletePolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 can no longer access the Thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - } - - @Test - public void subjectAddedViaAliasCanWriteThingWhenEntriesGrantWrite() { - final ThingId thingId = ThingId.of(idGenerator().withPrefixedRandomName("aliasWrite")); - - final PolicyId tmplPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("tmpl")); - final Policy tmplPolicy = buildTemplatePolicy(tmplPolicyId); - putPolicy(tmplPolicy).expectingHttpStatus(CREATED).fire(); - - final PolicyId thingPolicyId = PolicyId.of(thingId); - final Policy thingPolicy = buildImportingPolicyWithAlias(thingPolicyId, tmplPolicyId); - putPolicy(thingPolicy).expectingHttpStatus(CREATED).fire(); - - final Thing thing = ThingsModelFactory.newThingBuilder() - .setId(thingId) - .setPolicyId(thingPolicyId) - .setAttribute(JsonPointer.of("counter"), JsonValue.of(0)) - .build(); - putThing(TestConstants.API_V_2, thing, org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Add user2 via alias (template entries grant READ+WRITE on thing:/) - final Subject user2Subject = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - putPolicyEntrySubject(thingPolicyId, ALIAS_LABEL.toString(), - user2Subject.getId().toString(), user2Subject) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can now write to the Thing's attributes - final String attributePath = ResourcePathBuilder.forThing(thingId).attribute("counter").toString(); - put(dittoUrl(TestConstants.API_V_2, attributePath), "42") - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Verify the write took effect by reading the attribute - final String getAttrPath = ResourcePathBuilder.forThing(thingId).attribute("counter").toString(); - get(dittoUrl(TestConstants.API_V_2, getAttrPath)) - .expectingBody(containsCharSequence("42")) - .expectingHttpStatus(OK) - .fire(); - } - - // --- Helpers --- - - private Policy buildTemplatePolicy(final PolicyId templateId) { - return PoliciesModelFactory.newPolicyBuilder(templateId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel(TARGET_LABEL_1.toString()) - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor(TARGET_LABEL_1.toString(), Set.of(AllowedImportAddition.SUBJECTS)) - .forLabel(TARGET_LABEL_2.toString()) - .setSubject(defaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor(TARGET_LABEL_2.toString(), Set.of(AllowedImportAddition.SUBJECTS)) - .build(); - } - - private Policy buildImportingPolicyWithAlias(final PolicyId policyId, final PolicyId tmplPolicyId) { - final EntryAddition addition1 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_1, null, null); - final EntryAddition addition2 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_2, null, null); - final EntriesAdditions entriesAdditions = - PoliciesModelFactory.newEntriesAdditions(Arrays.asList(addition1, addition2)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - Arrays.asList(TARGET_LABEL_1, TARGET_LABEL_2), entriesAdditions); - final PolicyImport pImport = PoliciesModelFactory.newPolicyImport(tmplPolicyId, effectedImports); - - final List targets = Arrays.asList( - PoliciesModelFactory.newImportsAliasTarget(tmplPolicyId, TARGET_LABEL_1), - PoliciesModelFactory.newImportsAliasTarget(tmplPolicyId, TARGET_LABEL_2)); - final ImportsAlias a = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, targets); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setPolicyImports(PoliciesModelFactory.newPolicyImports(Collections.singletonList(pImport))) - .setImportsAliases(PoliciesModelFactory.newImportsAliases(Collections.singletonList(a))) - .build(); - } - - private void createTemplateThenImportingPolicy() { - putPolicy(templatePolicyId, templatePolicy).expectingHttpStatus(CREATED).fire(); - putPolicy(importingPolicyId, importingPolicy).expectingHttpStatus(CREATED).fire(); - } - - private static Subject defaultSubject() { - return serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - } - - private static PutMatcher putImportsAliases(final CharSequence policyId, final ImportsAliases importsAliases) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAliases().toString(); - return put(dittoUrl(TestConstants.API_V_2, path), importsAliases.toJsonString()) - .withLogging(LOGGER, "ImportsAliases"); - } - - private static GetMatcher getImportsAliases(final CharSequence policyId) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAliases().toString(); - return get(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "ImportsAliases"); - } - - private static DeleteMatcher deleteImportsAliases(final CharSequence policyId) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAliases().toString(); - return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "ImportsAliases"); - } - - private static PutMatcher putImportsAlias(final CharSequence policyId, final Label label, - final JsonObject aliasJson) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAlias(label).toString(); - return put(dittoUrl(TestConstants.API_V_2, path), aliasJson.toString()) - .withLogging(LOGGER, "ImportsAlias"); - } - - private static GetMatcher getImportsAlias(final CharSequence policyId, final Label label) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAlias(label).toString(); - return get(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "ImportsAlias"); - } - - private static DeleteMatcher deleteImportsAlias(final CharSequence policyId, final Label label) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImportsAlias(label).toString(); - return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "ImportsAlias"); - } - - private static DeleteMatcher deletePolicyImport(final CharSequence policyId, - final CharSequence importedPolicyId) { - final String path = ResourcePathBuilder.forPolicy(policyId).policyImport(importedPolicyId).toString(); - return delete(dittoUrl(TestConstants.API_V_2, path)).withLogging(LOGGER, "PolicyImport"); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/ThingsWithImportedPoliciesEntriesAdditionsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/ThingsWithImportedPoliciesEntriesAdditionsIT.java deleted file mode 100644 index 695105d..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/rest/ThingsWithImportedPoliciesEntriesAdditionsIT.java +++ /dev/null @@ -1,1022 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.rest; - -import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; -import static org.eclipse.ditto.base.model.common.HttpStatus.FORBIDDEN; -import static org.eclipse.ditto.base.model.common.HttpStatus.NOT_FOUND; -import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; -import static org.eclipse.ditto.base.model.common.HttpStatus.OK; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.List; -import java.util.Set; - -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyEntry; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Resource; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.TestConstants; -import org.junit.Before; -import org.junit.Test; - -/** - * Integration tests for Thing access via policy import {@code entriesAdditions}. - * Tests the connectivity use case where a template policy defines resource permissions and an importing - * policy adds user subjects via {@code entriesAdditions}. - */ -public final class ThingsWithImportedPoliciesEntriesAdditionsIT extends IntegrationTest { - - private PolicyId importedPolicyId; - private PolicyId importingPolicyId; - private Subject defaultSubject; - private Subject subject2; - - @Before - public void setUp() { - importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - } - - @Test - public void secondUserGainsThingAccessViaEntriesAdditions() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 can access the thing - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void secondUserLosesAccessWhenEntriesAdditionsRemoved() { - // Template grants thing:/ READ and allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // Verify user2 can access the thing initially - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Remove entriesAdditions by updating the import without additions - final PolicyImport importWithoutAdditions = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - putPolicyImport(importingPolicyId, importWithoutAdditions) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Verify user2 loses access - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void templateRevokePreservedWhenResourceAdditionsOverlap() { - // Template grants thing:/ READ but revokes WRITE, and allows both subject and resource additions - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of(WRITE)))), - ImportableType.IMPLICIT, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 and also tries to add grant WRITE on thing:/ - final Resource additionalResource = PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 should be able to READ (from template grant) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 should NOT be able to WRITE (template revoke should be preserved) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(FORBIDDEN) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void resourceAdditionGrantsWriteAccess() { - // Template grants thing:/ READ only, allows both subject and resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 AND grants WRITE on thing:/ via resource addition - final Resource writeResource = PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), - PoliciesModelFactory.newResources(writeResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (from template grant) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can WRITE (from resource addition granting WRITE on thing:/) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void additionsForMultipleImportedLabels() { - // Template has DEFAULT (thing:/ READ) and EXTRA (thing:/ WRITE), both allow subject additions - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - final PolicyEntry extraEntry = PoliciesModelFactory.newPolicyEntry("EXTRA", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of()))), - ImportableType.EXPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .set(extraEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 to both DEFAULT and EXTRA labels - final EntryAddition defaultAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntryAddition extraAddition = PoliciesModelFactory.newEntryAddition( - Label.of("EXTRA"), - PoliciesModelFactory.newSubjects(subject2), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions( - List.of(defaultAddition, extraAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT"), Label.of("EXTRA")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (from DEFAULT import with subject addition) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can WRITE (from EXTRA import with subject addition) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void templatePermissionChangeBecomesEffectiveForImportingPolicy() { - // Template grants thing:/ READ only, allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with the importing policy - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (from template grant) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 cannot WRITE (template only grants READ) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(FORBIDDEN) - .fire(); - - // Modify the template policy's DEFAULT entry to grant READ + WRITE - final PolicyEntry updatedDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - putPolicyEntry(importedPolicyId, updatedDefaultEntry) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 can now WRITE (template change is effective via the importing policy) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void reducingAllowedImportAdditionsRevokesResourceAdditionEffect() { - // Template grants thing:/ READ, allows both subject and resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 AND grants WRITE on thing:/ via resource addition - final Resource writeResource = PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(subject2), - PoliciesModelFactory.newResources(writeResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - final Policy importingPolicy = buildImportingPolicy(importingPolicyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (from template grant) and WRITE (from resource addition) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Reduce allowedImportAdditions on the template: remove "resources", keep only "subjects" - final PolicyEntry reducedDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, Set.of(AllowedImportAddition.SUBJECTS)); - putPolicyEntry(importedPolicyId, reducedDefaultEntry) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 can still READ (subject addition is still allowed) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can no longer WRITE (resource addition is no longer applied) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "updated").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(FORBIDDEN) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void templateRevokeAddedAfterImportOverridesResourceAdditionGrant() { - // Template grants thing:/ READ, allows both subject and resource additions - // DEFAULT entry has no subjects — only entriesAdditions subjects (user2) will be affected by the revoke - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 + WRITE resource addition on thing:/ - final Resource writeResource = PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final Policy importingPolicy = buildImportingPolicyWithSubjectAndResourceAdditions( - importingPolicyId, importedPolicyId, subject2, writeResource); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (from template grant) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can WRITE (from resource addition) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Modify template: add explicit REVOKE on WRITE (keep READ grant + allowedAdditions) - final PolicyEntry updatedDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of(WRITE)))), - ImportableType.IMPLICIT, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - putPolicyEntry(importedPolicyId, updatedDefaultEntry) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 can still READ - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 WRITE is now FORBIDDEN (template revoke overrides resource addition grant) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "updated").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(FORBIDDEN) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void templateEntryDeletionRevokesEntriesAdditionsAccess() { - // Template grants thing:/ READ, allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Delete DEFAULT entry from template - deletePolicyEntry(importedPolicyId, "DEFAULT") - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 loses access - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void templatePolicyDeletionRevokesImportedAccess() { - // Template grants thing:/ READ, allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds user2 subject via entriesAdditions - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Delete the entire template policy - deletePolicy(importedPolicyId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 loses access - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void multipleImportersFromSameTemplateAreIndependentlyAffected() { - // Template grants thing:/ READ, allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Two importing policies (different IDs), each adds subject2 via entriesAdditions - final PolicyId importingPolicyId2 = PolicyId.of(idGenerator().withPrefixedRandomName("importing2")); - - final Policy importingPolicy1 = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2); - putPolicy(importingPolicy1).expectingHttpStatus(CREATED).fire(); - - final Policy importingPolicy2 = buildImportingPolicyWithSubjectAdditions( - importingPolicyId2, importedPolicyId, subject2); - putPolicy(importingPolicy2).expectingHttpStatus(CREATED).fire(); - - // Create two things, one per importing policy - final String thingId1 = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId1) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - final String thingId2 = importingPolicyId2.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId2) - .set("policyId", importingPolicyId2.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ both things - getThing(TestConstants.API_V_2, thingId1) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - getThing(TestConstants.API_V_2, thingId2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Modify template: change DEFAULT to importable=NEVER - final PolicyEntry neverDefaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.NEVER, Set.of(AllowedImportAddition.SUBJECTS)); - putPolicyEntry(importedPolicyId, neverDefaultEntry) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 loses access to both things - getThing(TestConstants.API_V_2, thingId1) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - getThing(TestConstants.API_V_2, thingId2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NOT_FOUND) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId1) - .expectingHttpStatus(NO_CONTENT) - .fire(); - deleteThing(TestConstants.API_V_2, thingId2) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void subjectRetainsAccessFromOwnEntryWhenEntriesAdditionsRemoved() { - // Template grants thing:/ READ, allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy: adds subject2 via entriesAdditions AND has direct DIRECT_USER2 entry - final Policy importingPolicy = buildImportingPolicyWithSubjectAdditions( - importingPolicyId, importedPolicyId, subject2).toBuilder() - .forLabel("DIRECT_USER2") - .setSubject(subject2) - .setGrantedPermissions(thingResource("/"), READ) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Remove entriesAdditions (update import without additions) - final PolicyImport importWithoutAdditions = PoliciesModelFactory.newPolicyImport(importedPolicyId, - PoliciesModelFactory.newEffectedImportedLabels(List.of(Label.of("DEFAULT")))); - putPolicyImport(importingPolicyId, importWithoutAdditions) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 can still READ (from own DIRECT_USER2 entry in importing policy) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void resourceAdditionRespectsSubPathGranularity() { - // Template grants thing:/ READ, allows both subject and resource additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS, AllowedImportAddition.RESOURCES)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Importing policy adds subject2 + WRITE resource addition on thing:/attributes only - final Resource writeAttributesResource = PoliciesModelFactory.newResource(thingResource("/attributes"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final Policy importingPolicy = buildImportingPolicyWithSubjectAndResourceAdditions( - importingPolicyId, importedPolicyId, subject2, writeAttributesResource); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing with attributes and features - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("key", "value").build()) - .set("features", JsonObject.newBuilder() - .set("sensor", JsonObject.newBuilder() - .set("properties", JsonObject.newBuilder() - .set("value", 42) - .build()) - .build()) - .build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ whole thing (from template grant on thing:/) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can WRITE attributes (resource addition grants WRITE on thing:/attributes) - putAttributes(TestConstants.API_V_2, thingId, - JsonObject.newBuilder().set("key", "updated").build().toString()) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // user2 cannot WRITE features (no WRITE on thing:/features) - putFeatures(TestConstants.API_V_2, thingId, - JsonObject.newBuilder() - .set("sensor", JsonObject.newBuilder() - .set("properties", JsonObject.newBuilder() - .set("value", 99) - .build()) - .build()) - .build().toString()) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(FORBIDDEN) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - @Test - public void resourceAdditionWithoutSubjectAdditionAppliesToTemplateSubjects() { - // Template: DEFAULT entry has subject2 as subject with thing:/ READ, allows RESOURCES - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(subject2), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, Set.of(AllowedImportAddition.RESOURCES)); - final Policy importedPolicy = PoliciesModelFactory.newPolicyBuilder(importedPolicyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Import: WRITE resource addition on thing:/ with no subject addition - final Resource writeResource = PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(WRITE), List.of())); - final Policy importingPolicy = buildImportingPolicyWithResourceAdditions( - importingPolicyId, importedPolicyId, writeResource); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create a thing - final String thingId = importingPolicyId.toString(); - putThing(TestConstants.API_V_2, JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .expectingHttpStatus(CREATED) - .fire(); - - // user2 can READ (already a template subject) - getThing(TestConstants.API_V_2, thingId) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(OK) - .fire(); - - // user2 can WRITE (resource addition applies to existing template subjects) - putThing(TestConstants.API_V_2, - JsonObject.newBuilder() - .set("thingId", thingId) - .set("policyId", importingPolicyId.toString()) - .set("attributes", JsonObject.newBuilder().set("test", "value").build()) - .build(), - org.eclipse.ditto.base.model.json.JsonSchemaVersion.V_2) - .withConfiguredAuth(serviceEnv.getTestingContext2()) - .expectingHttpStatus(NO_CONTENT) - .fire(); - - // Cleanup - deleteThing(TestConstants.API_V_2, thingId) - .expectingHttpStatus(NO_CONTENT) - .fire(); - } - - private Policy buildImportedPolicy(final PolicyId policyId, - final Set allowedImportAdditions) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - private Policy buildImportingPolicy(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .build(); - } - - private Policy buildImportingPolicyWithSubjectAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - - private Policy buildImportingPolicyWithSubjectAndResourceAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Subject additionalSubject, - final Resource additionalResource) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - - private Policy buildImportingPolicyWithResourceAdditions(final PolicyId policyId, - final PolicyId importedPolicyId, final Resource additionalResource) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - null, - PoliciesModelFactory.newResources(additionalResource)); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - - return buildImportingPolicy(policyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportEntriesAdditionsWsIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportEntriesAdditionsWsIT.java deleted file mode 100644 index b3c9e77..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportEntriesAdditionsWsIT.java +++ /dev/null @@ -1,252 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.ws; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.ditto.base.model.common.HttpStatus.CREATED; -import static org.eclipse.ditto.base.model.common.HttpStatus.NO_CONTENT; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.testing.common.TestConstants.API_V_2; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.eclipse.ditto.base.model.common.HttpStatus; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.base.model.signals.commands.CommandResponse; -import org.eclipse.ditto.json.JsonObject; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyEntry; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImport; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImportResponse; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.TestConstants; -import org.eclipse.ditto.testing.common.ws.ThingsWebsocketClient; -import org.eclipse.ditto.things.model.Thing; -import org.eclipse.ditto.things.model.ThingId; -import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingNotAccessibleException; -import org.eclipse.ditto.things.model.signals.commands.modify.CreateThing; -import org.eclipse.ditto.things.model.signals.commands.modify.CreateThingResponse; -import org.eclipse.ditto.things.model.signals.commands.modify.DeleteThing; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing; -import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * WebSocket integration tests for policy import {@code entriesAdditions} and {@code allowedImportAdditions}. - */ -public final class PolicyImportEntriesAdditionsWsIT extends IntegrationTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(PolicyImportEntriesAdditionsWsIT.class); - private static final long TIMEOUT_SECONDS = 20L; - - private ThingsWebsocketClient clientUser1; - private ThingsWebsocketClient clientUser2; - private Subject defaultSubject; - private Subject subject2; - - @Before - public void setUp() { - defaultSubject = serviceEnv.getDefaultTestingContext().getOAuthClient().getDefaultSubject(); - subject2 = serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject(); - - clientUser1 = newTestWebsocketClient(serviceEnv.getDefaultTestingContext(), Map.of(), API_V_2); - clientUser2 = newTestWebsocketClient(serviceEnv.getTestingContext2(), Map.of(), API_V_2); - - clientUser1.connect("WsClient-User1-" + UUID.randomUUID()); - clientUser2.connect("WsClient-User2-" + UUID.randomUUID()); - } - - @After - public void tearDown() { - if (clientUser1 != null) { - clientUser1.disconnect(); - } - if (clientUser2 != null) { - clientUser2.disconnect(); - } - } - - @Test - public void modifyPolicyImportWithEntriesAdditionsViaWebSocket() throws Exception { - final PolicyId importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - final PolicyId importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - - // Create imported (template) policy via REST - allows subject additions - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy via REST (without imports initially) - final Policy importingPolicy = buildImportingPolicy(importingPolicyId); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Now modify the policy import via WebSocket with entriesAdditions - final PolicyImport policyImport = buildPolicyImportWithSubjectAdditions(importedPolicyId, subject2); - final ModifyPolicyImport modifyPolicyImport = ModifyPolicyImport.of( - importingPolicyId, policyImport, dittoHeaders()); - - final CommandResponse response = clientUser1.send(modifyPolicyImport) - .toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(response).isInstanceOf(ModifyPolicyImportResponse.class); - assertThat(response.getHttpStatus()).isEqualTo(HttpStatus.CREATED); - } - - @Test - public void retrieveThingViaWebSocketAfterSubjectAddedViaAdditions() throws Exception { - final PolicyId importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - final PolicyId importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - final ThingId thingId = ThingId.of(importingPolicyId); - - // Create imported (template) policy via REST - allows subject additions, grants thing:/ READ - final Policy importedPolicy = buildImportedPolicy(importedPolicyId, - Set.of(AllowedImportAddition.SUBJECTS)); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy with subject2 added via entriesAdditions - final PolicyImport policyImport = buildPolicyImportWithSubjectAdditions(importedPolicyId, subject2); - final Policy importingPolicy = buildImportingPolicy(importingPolicyId).toBuilder() - .setPolicyImport(policyImport) - .build(); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Create thing via WS with user1, referencing the existing importing policy - final Thing thing = Thing.newBuilder().setId(thingId).setPolicyId(importingPolicyId).build(); - final CreateThing createThing = CreateThing.of(thing, null, dittoHeaders()); - final CommandResponse createResponse = clientUser1.send(createThing) - .toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(createResponse).isInstanceOf(CreateThingResponse.class); - - // Verify user2 can retrieve the thing via WS (subject was added through entriesAdditions) - final RetrieveThing retrieveThing = RetrieveThing.of(thingId, dittoHeaders()); - final CommandResponse retrieveResponse = clientUser2.send(retrieveThing) - .toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(retrieveResponse).isInstanceOf(RetrieveThingResponse.class); - - // Cleanup - clientUser1.send(DeleteThing.of(thingId, dittoHeaders())); - } - - @Test - public void modifyPolicyImportWithDisallowedAdditionsViaWebSocket() throws Exception { - final PolicyId importedPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("imported")); - final PolicyId importingPolicyId = PolicyId.of(idGenerator().withPrefixedRandomName("importing")); - - // Create imported (template) policy via REST - NO allowedImportAdditions - final Policy importedPolicy = buildImportedPolicyWithoutAllowedAdditions(importedPolicyId); - putPolicy(importedPolicy).expectingHttpStatus(CREATED).fire(); - - // Create importing policy via REST (without imports initially) - final Policy importingPolicy = buildImportingPolicy(importingPolicyId); - putPolicy(importingPolicy).expectingHttpStatus(CREATED).fire(); - - // Attempt to modify policy import via WebSocket with disallowed entriesAdditions - final PolicyImport policyImport = buildPolicyImportWithSubjectAdditions(importedPolicyId, subject2); - final ModifyPolicyImport modifyPolicyImport = ModifyPolicyImport.of( - importingPolicyId, policyImport, dittoHeaders()); - - final CommandResponse response = clientUser1.send(modifyPolicyImport) - .toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - // Expect an error response - assertThat(response.getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST); - } - - private Policy buildImportedPolicy(final PolicyId policyId, - final Set allowedImportAdditions) { - - final PolicyEntry adminEntry = PoliciesModelFactory.newPolicyEntry("ADMIN", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(policyResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ, WRITE), List.of()))), - ImportableType.NEVER, Set.of()); - - final PolicyEntry defaultEntry = PoliciesModelFactory.newPolicyEntry("DEFAULT", - List.of(defaultSubject), - List.of(PoliciesModelFactory.newResource(thingResource("/"), - PoliciesModelFactory.newEffectedPermissions(List.of(READ), List.of()))), - ImportableType.IMPLICIT, allowedImportAdditions); - - return PoliciesModelFactory.newPolicyBuilder(policyId) - .set(adminEntry) - .set(defaultEntry) - .build(); - } - - private Policy buildImportedPolicyWithoutAllowedAdditions(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel("DEFAULT") - .setSubject(defaultSubject) - .setGrantedPermissions(thingResource("/"), READ) - .build(); - } - - private Policy buildImportingPolicy(final PolicyId policyId) { - return PoliciesModelFactory.newPolicyBuilder(policyId) - .forLabel("ADMIN") - .setSubject(defaultSubject) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .build(); - } - - private PolicyImport buildPolicyImportWithSubjectAdditions(final PolicyId importedPolicyId, - final Subject additionalSubject) { - - final EntryAddition entryAddition = PoliciesModelFactory.newEntryAddition( - Label.of("DEFAULT"), - PoliciesModelFactory.newSubjects(additionalSubject), null); - final EntriesAdditions additions = PoliciesModelFactory.newEntriesAdditions(List.of(entryAddition)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - List.of(Label.of("DEFAULT")), additions); - return PoliciesModelFactory.newPolicyImport(importedPolicyId, effectedImports); - } - - private static DittoHeaders dittoHeaders() { - return DittoHeaders.newBuilder() - .schemaVersion(JsonSchemaVersion.V_2) - .correlationId(UUID.randomUUID().toString()) - .build(); - } - -} diff --git a/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportsAliasesWebSocketIT.java b/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportsAliasesWebSocketIT.java deleted file mode 100644 index b19981b..0000000 --- a/system/src/test/java/org/eclipse/ditto/testing/system/things/ws/PolicyImportsAliasesWebSocketIT.java +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright (c) 2026 Contributors to the Eclipse Foundation - * - * See the NOTICE file(s) distributed with this work for additional - * information regarding copyright ownership. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.ditto.testing.system.things.ws; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.policyResource; -import static org.eclipse.ditto.policies.model.PoliciesResourceType.thingResource; -import static org.eclipse.ditto.testing.common.TestConstants.API_V_2; -import static org.eclipse.ditto.things.api.Permission.READ; -import static org.eclipse.ditto.things.api.Permission.WRITE; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -import org.eclipse.ditto.base.model.common.HttpStatus; -import org.eclipse.ditto.base.model.headers.DittoHeaders; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; -import org.eclipse.ditto.base.model.signals.Signal; -import org.eclipse.ditto.policies.model.EffectedImports; -import org.eclipse.ditto.policies.model.EntriesAdditions; -import org.eclipse.ditto.policies.model.EntryAddition; -import org.eclipse.ditto.policies.model.ImportsAlias; -import org.eclipse.ditto.policies.model.ImportsAliases; -import org.eclipse.ditto.policies.model.ImportsAliasTarget; -import org.eclipse.ditto.policies.model.ImportableType; -import org.eclipse.ditto.policies.model.Label; -import org.eclipse.ditto.policies.model.PoliciesModelFactory; -import org.eclipse.ditto.policies.model.Policy; -import org.eclipse.ditto.policies.model.PolicyId; -import org.eclipse.ditto.policies.model.PolicyImport; -import org.eclipse.ditto.policies.model.AllowedImportAddition; -import org.eclipse.ditto.policies.model.Subject; -import org.eclipse.ditto.policies.model.SubjectType; -import org.eclipse.ditto.policies.model.Subjects; -import org.eclipse.ditto.policies.model.signals.commands.modify.CreatePolicy; -import org.eclipse.ditto.policies.model.signals.commands.modify.CreatePolicyResponse; -import org.eclipse.ditto.policies.model.signals.commands.modify.DeleteImportsAlias; -import org.eclipse.ditto.policies.model.signals.commands.modify.DeleteImportsAliasResponse; -import org.eclipse.ditto.policies.model.signals.commands.modify.DeleteImportsAliases; -import org.eclipse.ditto.policies.model.signals.commands.modify.DeleteImportsAliasesResponse; -import org.eclipse.ditto.policies.model.signals.commands.modify.DeletePolicy; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyImportsAlias; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyImportsAliasResponse; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyImportsAliases; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyImportsAliasesResponse; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifySubjects; -import org.eclipse.ditto.policies.model.signals.commands.modify.ModifySubjectsResponse; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveImportsAlias; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveImportsAliasResponse; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveImportsAliases; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveImportsAliasesResponse; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveSubjects; -import org.eclipse.ditto.policies.model.signals.commands.query.RetrieveSubjectsResponse; -import org.eclipse.ditto.testing.common.IntegrationTest; -import org.eclipse.ditto.testing.common.ServiceEnvironment; -import org.eclipse.ditto.testing.common.TestingContext; -import org.eclipse.ditto.testing.common.config.TestConfig; -import org.eclipse.ditto.testing.common.config.TestEnvironment; -import org.eclipse.ditto.testing.common.ws.ThingsWebsocketClient; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * WebSocket / Ditto Protocol integration tests for policy import aliases — verifying that CRUD operations and - * subject fan-out work correctly through the WebSocket channel. - */ -public final class PolicyImportsAliasesWebSocketIT extends IntegrationTest { - - private static final Logger LOGGER = LoggerFactory.getLogger(PolicyImportsAliasesWebSocketIT.class); - private static final long TIMEOUT_SECONDS = 15; - - private static final Label ALIAS_LABEL = Label.of("operator"); - private static final Label TARGET_LABEL_1 = Label.of("operator-reactor"); - private static final Label TARGET_LABEL_2 = Label.of("operator-turbine"); - - private ThingsWebsocketClient wsClient; - private TestingContext testingContext; - - private PolicyId templatePolicyId; - private PolicyId importingPolicyId; - private ImportsAlias alias; - private ImportsAliases aliases; - - @Before - public void setUp() { - if (TestConfig.getInstance().getTestEnvironment() == TestEnvironment.DEPLOYMENT) { - testingContext = serviceEnv.getDefaultTestingContext(); - } else { - testingContext = TestingContext.withGeneratedMockClient( - ServiceEnvironment.createSolutionWithRandomUsernameRandomNamespace(), TEST_CONFIG); - } - - wsClient = newTestWebsocketClient(testingContext, new HashMap<>(), API_V_2); - wsClient.connect("PolicyImportsAliasesWS-" + UUID.randomUUID()); - - templatePolicyId = PolicyId.inNamespaceWithRandomName( - testingContext.getSolution().getDefaultNamespace()); - importingPolicyId = PolicyId.inNamespaceWithRandomName( - testingContext.getSolution().getDefaultNamespace()); - - final List targets = Arrays.asList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1), - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_2) - ); - alias = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, targets); - aliases = PoliciesModelFactory.newImportsAliases(Collections.singletonList(alias)); - } - - @After - public void tearDown() { - if (wsClient != null) { - try { - wsClient.send(DeletePolicy.of(importingPolicyId, headers())).toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (final Exception e) { - LOGGER.debug("Cleanup of importing policy failed: {}", e.getMessage()); - } - try { - wsClient.send(DeletePolicy.of(templatePolicyId, headers())).toCompletableFuture() - .get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - } catch (final Exception e) { - LOGGER.debug("Cleanup of template policy failed: {}", e.getMessage()); - } - wsClient.disconnect(); - } - } - - // --- CRUD via WebSocket --- - - @Test - public void createPolicyWithAliasesAndRetrieveViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - final Signal response = wsClient.send( - RetrieveImportsAliases.of(importingPolicyId, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(response).isInstanceOf(RetrieveImportsAliasesResponse.class); - final RetrieveImportsAliasesResponse retrieveResponse = (RetrieveImportsAliasesResponse) response; - assertThat(retrieveResponse.getImportsAliases()).isEqualTo(aliases); - } - - @Test - public void retrieveSingleImportsAliasViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - final Signal response = wsClient.send( - RetrieveImportsAlias.of(importingPolicyId, ALIAS_LABEL, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(response).isInstanceOf(RetrieveImportsAliasResponse.class); - final RetrieveImportsAliasResponse retrieveResponse = (RetrieveImportsAliasResponse) response; - assertThat(retrieveResponse.getEntity()).isEqualTo(alias.toJson()); - } - - @Test - public void modifySingleImportsAliasViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - // Modify alias to have only one target - final ImportsAlias modifiedAlias = PoliciesModelFactory.newImportsAlias(ALIAS_LABEL, - Collections.singletonList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1))); - - final Signal modifyResponse = wsClient.send( - ModifyImportsAlias.of(importingPolicyId, modifiedAlias, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(modifyResponse).isInstanceOf(ModifyImportsAliasResponse.class); - - // Verify the modification - final Signal retrieveResponse = wsClient.send( - RetrieveImportsAlias.of(importingPolicyId, ALIAS_LABEL, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(retrieveResponse).isInstanceOf(RetrieveImportsAliasResponse.class); - final RetrieveImportsAliasResponse verifyResponse = (RetrieveImportsAliasResponse) retrieveResponse; - assertThat(verifyResponse.getEntity()).isEqualTo(modifiedAlias.toJson()); - } - - @Test - public void modifyAllImportsAliasesViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - // Replace all aliases with a new one - final Label newLabel = Label.of("inspector"); - final ImportsAlias newAlias = PoliciesModelFactory.newImportsAlias(newLabel, - Collections.singletonList( - PoliciesModelFactory.newImportsAliasTarget(templatePolicyId, TARGET_LABEL_1))); - final ImportsAliases newAliases = PoliciesModelFactory.newImportsAliases(Collections.singletonList(newAlias)); - - final Signal modifyResponse = wsClient.send( - ModifyImportsAliases.of(importingPolicyId, newAliases, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(modifyResponse).isInstanceOf(ModifyImportsAliasesResponse.class); - - // Verify - final Signal retrieveResponse = wsClient.send( - RetrieveImportsAliases.of(importingPolicyId, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(retrieveResponse).isInstanceOf(RetrieveImportsAliasesResponse.class); - assertThat(((RetrieveImportsAliasesResponse) retrieveResponse).getImportsAliases()).isEqualTo(newAliases); - } - - @Test - public void deleteSingleImportsAliasViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - final Signal deleteResponse = wsClient.send( - DeleteImportsAlias.of(importingPolicyId, ALIAS_LABEL, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(deleteResponse).isInstanceOf(DeleteImportsAliasResponse.class); - - // Verify deletion — retrieve should return empty aliases - final Signal retrieveResponse = wsClient.send( - RetrieveImportsAliases.of(importingPolicyId, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(retrieveResponse).isInstanceOf(RetrieveImportsAliasesResponse.class); - assertThat(((RetrieveImportsAliasesResponse) retrieveResponse).getImportsAliases().isEmpty()).isTrue(); - } - - @Test - public void deleteAllImportsAliasesViaWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - final Signal deleteResponse = wsClient.send( - DeleteImportsAliases.of(importingPolicyId, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(deleteResponse).isInstanceOf(DeleteImportsAliasesResponse.class); - - // Verify - final Signal retrieveResponse = wsClient.send( - RetrieveImportsAliases.of(importingPolicyId, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(retrieveResponse).isInstanceOf(RetrieveImportsAliasesResponse.class); - assertThat(((RetrieveImportsAliasesResponse) retrieveResponse).getImportsAliases().isEmpty()).isTrue(); - } - - // --- Subject fan-out via WebSocket --- - - @Test - public void modifySubjectsViaAliasLabelFansOutThroughWebSocket() throws Exception { - createTemplateThenImportingPolicy(); - - final Subject newSubject = Subject.newInstance( - serviceEnv.getTestingContext2().getOAuthClient().getDefaultSubject().getId(), - SubjectType.GENERATED); - final Subjects subjects = Subjects.newInstance(newSubject); - - // ModifySubjects using the alias label → should fan out to all targets - final Signal modifyResponse = wsClient.send( - ModifySubjects.of(importingPolicyId, ALIAS_LABEL, subjects, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(modifyResponse).isInstanceOf(ModifySubjectsResponse.class); - - // Retrieve subjects through alias — should return subjects from first target - final Signal retrieveResponse = wsClient.send( - RetrieveSubjects.of(importingPolicyId, ALIAS_LABEL, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - - assertThat(retrieveResponse).isInstanceOf(RetrieveSubjectsResponse.class); - final RetrieveSubjectsResponse subjectsResponse = (RetrieveSubjectsResponse) retrieveResponse; - assertThat(subjectsResponse.getSubjects().getSubject(newSubject.getId())).isPresent(); - } - - // --- Helpers --- - - private DittoHeaders headers() { - return DittoHeaders.newBuilder() - .schemaVersion(JsonSchemaVersion.V_2) - .randomCorrelationId() - .build(); - } - - private void createTemplateThenImportingPolicy() throws Exception { - // Create template policy via REST (simpler setup) - final Policy templatePolicy = PoliciesModelFactory.newPolicyBuilder(templatePolicyId) - .forLabel("ADMIN") - .setSubject(testingContext.getOAuthClient().getDefaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setImportable(ImportableType.NEVER) - .forLabel("operator-reactor") - .setSubject(testingContext.getOAuthClient().getDefaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor("operator-reactor", Set.of(AllowedImportAddition.SUBJECTS)) - .forLabel("operator-turbine") - .setSubject(testingContext.getOAuthClient().getDefaultSubject()) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setImportable(ImportableType.EXPLICIT) - .setAllowedImportAdditionsFor("operator-turbine", Set.of(AllowedImportAddition.SUBJECTS)) - .build(); - - final Signal createTemplateResponse = wsClient.send( - CreatePolicy.of(templatePolicy, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(createTemplateResponse).isInstanceOf(CreatePolicyResponse.class); - - // Build import with entries additions - final EntryAddition addition1 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_1, null, null); - final EntryAddition addition2 = PoliciesModelFactory.newEntryAddition(TARGET_LABEL_2, null, null); - final EntriesAdditions entriesAdditions = - PoliciesModelFactory.newEntriesAdditions(Arrays.asList(addition1, addition2)); - final EffectedImports effectedImports = PoliciesModelFactory.newEffectedImportedLabels( - Arrays.asList(TARGET_LABEL_1, TARGET_LABEL_2), entriesAdditions); - final PolicyImport policyImport = PoliciesModelFactory.newPolicyImport(templatePolicyId, effectedImports); - - // Create importing policy with alias via WebSocket - final Policy importingPolicy = PoliciesModelFactory.newPolicyBuilder(importingPolicyId) - .forLabel("ADMIN") - .setSubject(testingContext.getOAuthClient().getDefaultSubject()) - .setGrantedPermissions(policyResource("/"), READ, WRITE) - .setGrantedPermissions(thingResource("/"), READ, WRITE) - .setPolicyImports(PoliciesModelFactory.newPolicyImports(Collections.singletonList(policyImport))) - .setImportsAliases(aliases) - .build(); - - final Signal createImportingResponse = wsClient.send( - CreatePolicy.of(importingPolicy, headers()) - ).toCompletableFuture().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); - assertThat(createImportingResponse).isInstanceOf(CreatePolicyResponse.class); - } - -}