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); - } - -}