Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement pre-enforcer to enforce read access on imported policies
Co-authored-by: Kalin Kostashki <kalin.kostashki@bosch.io> Signed-off-by: Dominik Guggemos <dominik.guggemos@bosch.io>
- Loading branch information
Showing
9 changed files
with
673 additions
and
6 deletions.
There are no files selected for viewing
135 changes: 135 additions & 0 deletions
135
...nt/src/main/java/org/eclipse/ditto/policies/enforcement/pre/PolicyImportsPreEnforcer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/* | ||
* Copyright (c) 2022 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.policies.enforcement.pre; | ||
|
||
import java.util.Set; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.CompletionStage; | ||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import org.eclipse.ditto.base.model.auth.AuthorizationContext; | ||
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException; | ||
import org.eclipse.ditto.base.model.headers.DittoHeaders; | ||
import org.eclipse.ditto.base.model.signals.Signal; | ||
import org.eclipse.ditto.policies.api.Permission; | ||
import org.eclipse.ditto.policies.enforcement.PolicyEnforcer; | ||
import org.eclipse.ditto.policies.enforcement.PolicyEnforcerProvider; | ||
import org.eclipse.ditto.policies.model.PoliciesModelFactory; | ||
import org.eclipse.ditto.policies.model.PolicyId; | ||
import org.eclipse.ditto.policies.model.PolicyImport; | ||
import org.eclipse.ditto.policies.model.ResourceKey; | ||
import org.eclipse.ditto.policies.model.enforcers.Enforcer; | ||
import org.eclipse.ditto.policies.model.signals.commands.exceptions.PolicyImportNotAccessibleException; | ||
import org.eclipse.ditto.policies.model.signals.commands.exceptions.PolicyNotAccessibleException; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.CreatePolicy; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicy; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImport; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImports; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.PolicyModifyCommand; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.typesafe.config.Config; | ||
|
||
import akka.actor.ActorSystem; | ||
|
||
/** | ||
* Pre-Enforcer for authorizing modifications to policy imports. | ||
*/ | ||
public class PolicyImportsPreEnforcer implements PreEnforcer { | ||
|
||
private static final String POLICY_RESOURCE = "policy"; | ||
public static final String ENTRIES_PREFIX = "/entries/"; | ||
private final PolicyEnforcerProvider policyEnforcerProvider; | ||
|
||
// TOOO DG logging | ||
private static final Logger LOGGER = LoggerFactory.getLogger(PolicyImportsPreEnforcer.class); | ||
|
||
/** | ||
* Constructs a new instance of PolicyImportsPreEnforcer extension. | ||
* | ||
* @param actorSystem the actor system in which to load the extension. | ||
* @param config the configuration for this extension. | ||
*/ | ||
@SuppressWarnings("unused") | ||
public PolicyImportsPreEnforcer(final ActorSystem actorSystem, final Config config) { | ||
// TODO DG make PolicyEnforcerProvider an extension | ||
policyEnforcerProvider = PolicyEnforcerProvider.getInstance(actorSystem); | ||
} | ||
|
||
PolicyImportsPreEnforcer(final PolicyEnforcerProvider policyEnforcerProvider) { | ||
this.policyEnforcerProvider = policyEnforcerProvider; | ||
} | ||
|
||
@Override | ||
public CompletionStage<Signal<?>> apply(final Signal<?> signal) { | ||
if (signal instanceof ModifyPolicy modifyPolicy) { | ||
return doApply(modifyPolicy.getPolicy().getPolicyImports().stream(), modifyPolicy); | ||
} else if (signal instanceof CreatePolicy createPolicy) { | ||
return doApply(createPolicy.getPolicy().getPolicyImports().stream(), createPolicy); | ||
} else if (signal instanceof ModifyPolicyImports modifyPolicyImports) { | ||
return doApply(modifyPolicyImports.getPolicyImports().stream(), modifyPolicyImports); | ||
} else if (signal instanceof ModifyPolicyImport modifyPolicyImport) { | ||
return doApply(Stream.of(modifyPolicyImport.getPolicyImport()), modifyPolicyImport); | ||
} else { | ||
return CompletableFuture.completedStage(signal); | ||
} | ||
} | ||
|
||
private CompletionStage<Signal<?>> doApply(final Stream<PolicyImport> policyImportStream, | ||
final PolicyModifyCommand<?> command) { | ||
final DittoHeaders dittoHeaders = command.getDittoHeaders(); | ||
return policyImportStream.map( | ||
policyImport -> getPolicyEnforcer(policyImport.getImportedPolicyId(), dittoHeaders).thenApply( | ||
importedPolicyEnforcer -> authorize(command, importedPolicyEnforcer.getEnforcer(), | ||
policyImport))) | ||
.reduce(CompletableFuture.completedStage(true), (s1, s2) -> s1.thenCombine(s2, (b1, b2) -> b1 && b2)) | ||
.thenApply(ignored -> command); | ||
} | ||
|
||
private CompletionStage<PolicyEnforcer> getPolicyEnforcer(final PolicyId policyId, | ||
final DittoHeaders dittoHeaders) { | ||
return policyEnforcerProvider.getPolicyEnforcer(policyId) | ||
.thenApply(policyEnforcerOpt -> policyEnforcerOpt.orElseThrow( | ||
() -> PolicyNotAccessibleException.newBuilder(policyId).dittoHeaders(dittoHeaders).build())); | ||
} | ||
|
||
private static Set<ResourceKey> getResourceKeys(final PolicyImport policyImport) { | ||
return policyImport.getEffectedImports().orElse(PoliciesModelFactory.emptyEffectedImportedEntries()) | ||
.getImportedLabels() | ||
.stream() | ||
.map(l -> ENTRIES_PREFIX + l) | ||
.map(path -> ResourceKey.newInstance(POLICY_RESOURCE, path)) | ||
.collect(Collectors.toSet()); | ||
} | ||
|
||
private boolean authorize(final PolicyModifyCommand<?> command, final Enforcer enforcer, | ||
final PolicyImport policyImport) { | ||
final Set<ResourceKey> resourceKeys = getResourceKeys(policyImport); | ||
final AuthorizationContext authorizationContext = command.getDittoHeaders().getAuthorizationContext(); | ||
final boolean hasAccess = | ||
enforcer.hasUnrestrictedPermissions(resourceKeys, authorizationContext, Permission.READ); | ||
if (!hasAccess) { | ||
throw errorForPolicyModifyCommand(command, policyImport); | ||
} else { | ||
return true; | ||
} | ||
} | ||
|
||
private static DittoRuntimeException errorForPolicyModifyCommand(final PolicyModifyCommand<?> policyModifyCommand, | ||
final PolicyImport policyImport) { | ||
return PolicyImportNotAccessibleException.newBuilder(policyModifyCommand.getEntityId(), | ||
policyImport.getImportedPolicyId()).build(); | ||
} | ||
} |
235 changes: 235 additions & 0 deletions
235
...rc/test/java/org/eclipse/ditto/policies/enforcement/pre/PolicyImportsPreEnforcerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
/* | ||
* Copyright (c) 2022 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.policies.enforcement.pre; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORTED; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORTED_POLICY_ID; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORTING; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORTING_POLICY_ID; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORT_NOT_FOUND; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.IMPORT_NOT_FOUND_POLICY_ID; | ||
import static org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcerTest.Policies.KNOWN_IDS; | ||
import static org.mockito.ArgumentMatchers.argThat; | ||
import static org.mockito.Mockito.when; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.concurrent.CompletableFuture; | ||
import java.util.concurrent.CompletionException; | ||
import java.util.stream.Stream; | ||
|
||
import org.eclipse.ditto.base.model.auth.AuthorizationContext; | ||
import org.eclipse.ditto.base.model.auth.AuthorizationModelFactory; | ||
import org.eclipse.ditto.base.model.headers.DittoHeaders; | ||
import org.eclipse.ditto.base.model.signals.Signal; | ||
import org.eclipse.ditto.json.JsonObject; | ||
import org.eclipse.ditto.policies.enforcement.PolicyEnforcer; | ||
import org.eclipse.ditto.policies.enforcement.PolicyEnforcerProvider; | ||
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.signals.commands.exceptions.PolicyImportNotAccessibleException; | ||
import org.eclipse.ditto.policies.model.signals.commands.exceptions.PolicyNotAccessibleException; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.CreatePolicy; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicy; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImport; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.ModifyPolicyImports; | ||
import org.eclipse.ditto.policies.model.signals.commands.modify.PolicyModifyCommand; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtensionContext; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.ArgumentsProvider; | ||
import org.junit.jupiter.params.provider.ArgumentsSource; | ||
import org.mockito.Mockito; | ||
|
||
/** | ||
* Tests {@link org.eclipse.ditto.policies.enforcement.pre.PolicyImportsPreEnforcer}. | ||
*/ | ||
class PolicyImportsPreEnforcerTest { | ||
|
||
private static final AuthorizationContext AUTH_CONTEXT_SUBJECT_ALLOWED = AuthorizationModelFactory.newAuthContext( | ||
JsonObject.of(""" | ||
{ | ||
"type" : "unspecified", | ||
"subjects" : ["ditto:subject1"] | ||
} | ||
""")); | ||
private static final AuthorizationContext AUTH_CONTEXT_SUBJECT_FORBIDDEN = AuthorizationModelFactory.newAuthContext( | ||
JsonObject.of(""" | ||
{ | ||
"type" : "unspecified", | ||
"subjects" : ["ditto:subject2"] | ||
} | ||
""")); | ||
private PolicyImportsPreEnforcer policyImportsPreEnforcer; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
PolicyEnforcerProvider policyEnforcerProvider = Mockito.mock(PolicyEnforcerProvider.class); | ||
|
||
when(policyEnforcerProvider.getPolicyEnforcer(IMPORTED_POLICY_ID)) | ||
.thenReturn(CompletableFuture.completedStage(Optional.of(PolicyEnforcer.of(IMPORTED)))); | ||
when(policyEnforcerProvider.getPolicyEnforcer(IMPORTING_POLICY_ID)) | ||
.thenReturn(CompletableFuture.completedStage(Optional.of(PolicyEnforcer.of(IMPORTING)))); | ||
when(policyEnforcerProvider.getPolicyEnforcer(IMPORT_NOT_FOUND_POLICY_ID)) | ||
.thenReturn(CompletableFuture.completedStage(Optional.of(PolicyEnforcer.of(IMPORT_NOT_FOUND)))); | ||
when(policyEnforcerProvider.getPolicyEnforcer(argThat(id -> !KNOWN_IDS.contains(id)))) | ||
.thenReturn(CompletableFuture.completedStage(Optional.empty())); | ||
|
||
policyImportsPreEnforcer = new PolicyImportsPreEnforcer(policyEnforcerProvider); | ||
} | ||
|
||
@ParameterizedTest | ||
@ArgumentsSource(PolicyModifyCommandsProvider.class) | ||
void testSubjectAllowedToReadImportedPolicy(final PolicyModifyCommandsProvider.Outcome outcome, | ||
final PolicyModifyCommand<?> command) { | ||
final CompletableFuture<Signal<?>> applyFuture = policyImportsPreEnforcer.apply(command).toCompletableFuture(); | ||
switch (outcome) { | ||
case SUCCESS -> { | ||
final Signal<?> signal = applyFuture.join(); | ||
assertThat(signal).isSameAs(command); | ||
} | ||
case ERROR -> { | ||
assertThatExceptionOfType(CompletionException.class) | ||
.isThrownBy(applyFuture::join) | ||
.withCauseInstanceOf(PolicyImportNotAccessibleException.class); | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
void testEnforcerOfImportedPolicyNotFound() { | ||
final DittoHeaders dittoHeaders = | ||
DittoHeaders.newBuilder().authorizationContext(AUTH_CONTEXT_SUBJECT_FORBIDDEN).build(); | ||
|
||
final ModifyPolicy modifyPolicy = ModifyPolicy.of(IMPORT_NOT_FOUND_POLICY_ID, IMPORT_NOT_FOUND, dittoHeaders); | ||
|
||
final CompletableFuture<Signal<?>> signalFuture = | ||
policyImportsPreEnforcer.apply(modifyPolicy).toCompletableFuture(); | ||
|
||
assertThatExceptionOfType(CompletionException.class) | ||
.isThrownBy(signalFuture::join) | ||
.withCauseInstanceOf(PolicyNotAccessibleException.class); | ||
} | ||
|
||
private static class PolicyModifyCommandsProvider implements ArgumentsProvider { | ||
|
||
private PolicyModifyCommandsProvider() { | ||
} | ||
|
||
@Override | ||
public Stream<? extends Arguments> provideArguments(ExtensionContext context) { | ||
|
||
return Arrays.stream(Outcome.values()) | ||
.flatMap(outcome -> Stream.of( | ||
Arguments.of(outcome, CreatePolicy.of(IMPORTING, getDittoHeaders(outcome))), | ||
Arguments.of(outcome, | ||
ModifyPolicy.of(IMPORTING_POLICY_ID, IMPORTING, getDittoHeaders(outcome))), | ||
Arguments.of(outcome, | ||
ModifyPolicyImports.of(IMPORTING_POLICY_ID, IMPORTING.getPolicyImports(), | ||
getDittoHeaders(outcome))), | ||
Arguments.of(outcome, ModifyPolicyImport.of(IMPORTING_POLICY_ID, | ||
IMPORTING.getPolicyImports().getPolicyImport(IMPORTED_POLICY_ID).orElseThrow(), | ||
getDittoHeaders(outcome))) | ||
)); | ||
} | ||
|
||
private static DittoHeaders getDittoHeaders(final Outcome outcome) { | ||
return switch (outcome) { | ||
case SUCCESS -> DittoHeaders.newBuilder().authorizationContext(AUTH_CONTEXT_SUBJECT_ALLOWED).build(); | ||
case ERROR -> DittoHeaders.newBuilder().authorizationContext(AUTH_CONTEXT_SUBJECT_FORBIDDEN).build(); | ||
}; | ||
} | ||
|
||
enum Outcome { | ||
SUCCESS, | ||
ERROR | ||
} | ||
} | ||
|
||
static class Policies { | ||
static final Policy IMPORTING = PoliciesModelFactory.newPolicy(""" | ||
{ | ||
"policyId": "test:importing", | ||
"entries" : { | ||
"DEFAULT" : { | ||
"subjects": { | ||
"ditto:subject1" : { "type": "test" } | ||
}, | ||
"resources": { | ||
"thing:/attributes": { "grant": [ "READ", "WRITE" ], "revoke": [] } | ||
} | ||
} | ||
}, | ||
"imports": { | ||
"test:imported": {"entries":["IMPORT"]} | ||
} | ||
} | ||
"""); | ||
static final Policy IMPORTED = PoliciesModelFactory.newPolicy(""" | ||
{ | ||
"policyId": "test:imported", | ||
"entries" : { | ||
"DEFAULT" : { | ||
"subjects": { | ||
"ditto:subject2" : { "type": "test" } | ||
}, | ||
"resources": { | ||
"thing:/": { "grant": [ "READ", "WRITE" ], "revoke": [] } | ||
}, | ||
"importable":"never" | ||
}, | ||
"IMPORT" : { | ||
"subjects": { | ||
"ditto:subject1" : { "type": "test" } | ||
}, | ||
"resources": { | ||
"policy:/entries/IMPORT": { "grant": [ "READ" ], "revoke": [] } | ||
}, | ||
"importable":"explicit" | ||
} | ||
} | ||
} | ||
"""); | ||
static final Policy IMPORT_NOT_FOUND = PoliciesModelFactory.newPolicy(""" | ||
{ | ||
"policyId": "test:import.not.found", | ||
"entries" : { | ||
"DEFAULT" : { | ||
"subjects": { | ||
"ditto:subject1" : { "type": "test" } | ||
}, | ||
"resources": { | ||
"policy:/": { "grant": [ "READ", "WRITE" ], "revoke": [] } | ||
} | ||
} | ||
}, | ||
"imports": { | ||
"test:notfound": {"entries":["IMPORT"]} | ||
} | ||
} | ||
"""); | ||
static final PolicyId IMPORTING_POLICY_ID = IMPORTING.getEntityId().orElseThrow(); | ||
static final PolicyId IMPORTED_POLICY_ID = IMPORTED.getEntityId().orElseThrow(); | ||
static final PolicyId IMPORT_NOT_FOUND_POLICY_ID = IMPORT_NOT_FOUND.getEntityId().orElseThrow(); | ||
|
||
static final Collection<PolicyId> KNOWN_IDS = | ||
List.of(IMPORTED_POLICY_ID, IMPORTING_POLICY_ID, IMPORT_NOT_FOUND_POLICY_ID); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.