Skip to content

Commit

Permalink
implement pre-enforcer to enforce read access on imported policies
Browse files Browse the repository at this point in the history
Co-authored-by: Kalin Kostashki <kalin.kostashki@bosch.io>
Signed-off-by: Dominik Guggemos <dominik.guggemos@bosch.io>
  • Loading branch information
dguggemos and Kalin Kostashki committed Sep 26, 2022
1 parent fe08fe8 commit a70b3aa
Show file tree
Hide file tree
Showing 9 changed files with 673 additions and 6 deletions.
@@ -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();
}
}
@@ -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);
}
}
Expand Up @@ -33,7 +33,7 @@
public interface PolicyActionCommand<T extends PolicyActionCommand<T>> extends PolicyCommand<T>, WithOptionalEntity {

/**
* Path of Policy actions as part of the the {@link #getResourcePath()}.
* Path of Policy actions as part of the {@link #getResourcePath()}.
*/
JsonPointer RESOURCE_PATH_ACTIONS = JsonPointer.of("actions");

Expand Down

0 comments on commit a70b3aa

Please sign in to comment.