Skip to content

Commit

Permalink
Merge pull request #1427 from paullatzelsperger/feat/1426_enable_addi…
Browse files Browse the repository at this point in the history
…tional_operators

feat: enable support for additional Operators in the BPN permission function
  • Loading branch information
paullatzelsperger committed Jul 17, 2024
2 parents 1f40f32 + 711d187 commit 32344e2
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,42 @@
import org.eclipse.edc.policy.model.Operator;
import org.eclipse.edc.policy.model.Permission;
import org.eclipse.edc.spi.agent.ParticipantAgent;
import org.eclipse.edc.spi.result.Failure;
import org.eclipse.edc.spi.result.Result;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

import static java.lang.String.format;
import static org.eclipse.edc.policy.model.Operator.EQ;
import static org.eclipse.edc.policy.model.Operator.HAS_PART;
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* AtomicConstraintFunction to validate business partner numbers for edc permissions.
*/
public class BusinessPartnerNumberPermissionFunction implements AtomicConstraintFunction<Permission> {

public BusinessPartnerNumberPermissionFunction() {
}
private static final List<Operator> SUPPORTED_OPERATORS = Arrays.asList(
EQ,
Operator.IN,
Operator.NEQ,
Operator.IS_ANY_OF,
Operator.IS_A,
Operator.IS_NONE_OF,
Operator.IS_ALL_OF,
Operator.HAS_PART
);

@Override
public boolean evaluate(Operator operator, Object rightValue, Permission rule, PolicyContext context) {
if (operator != Operator.EQ) {
var message = format("As operator only 'EQ' is supported. Unsupported operator: '%s'", operator);

if (!SUPPORTED_OPERATORS.contains(operator)) {
var message = "Operator %s is not supported. Supported operators: %s".formatted(operator, SUPPORTED_OPERATORS);
context.reportProblem(message);
return false;
}
Expand All @@ -58,19 +76,51 @@ public boolean evaluate(Operator operator, Object rightValue, Permission rule, P
return false;
}

if ((rightValue instanceof String businessPartnerNumberStr)) {
if (businessPartnerNumberStr.equals(identity)) {
return true;
} else {
context.reportProblem("Identity of the participant not matching the expected one: " + businessPartnerNumberStr);
return false;
}
} else {
var message = format("Invalid right operand value: expected 'String' but got '%s'",
Optional.of(rightValue).map(Object::getClass).map(Class::getName).orElse(null));
context.reportProblem(message);
return switch (operator) {
case EQ, IS_ALL_OF -> checkEquality(identity, rightValue, operator)
.orElse(reportFailure(context));
case NEQ -> checkEquality(identity, rightValue, operator)
.map(b -> !b)
.orElse(reportFailure(context));
case HAS_PART -> checkStringContains(identity, rightValue)
.orElse(reportFailure(context));
case IN, IS_A, IS_ANY_OF ->
checkListContains(identity, rightValue, operator).orElse(reportFailure(context));
case IS_NONE_OF -> checkListContains(identity, rightValue, operator)
.map(b -> !b)
.orElse(reportFailure(context));
default -> false;
};
}

private @NotNull Function<Failure, Boolean> reportFailure(PolicyContext context) {
return f -> {
context.reportProblem(f.getFailureDetail());
return false;
};
}

private Result<Boolean> checkListContains(String identity, Object rightValue, Operator operator) {
if (rightValue instanceof List numbers) {
return success(numbers.contains(identity));
}
return failure("Invalid right-value: operator '%s' requires a 'List' but got a '%s'".formatted(operator, Optional.of(rightValue).map(Object::getClass).map(Class::getName).orElse(null)));
}

private Result<Boolean> checkStringContains(String identity, Object rightValue) {
if (rightValue instanceof String bpnString) {
return success(identity.contains(bpnString));
}
return failure("Invalid right-value: operator '%s' requires a 'String' but got a '%s'".formatted(HAS_PART, Optional.of(rightValue).map(Object::getClass).map(Class::getName).orElse(null)));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private Result<Boolean> checkEquality(String identity, Object rightValue, Operator operator) {
if (rightValue instanceof String bpnString) {
return success(Objects.equals(identity, bpnString));
} else if (rightValue instanceof List bpnList) {
return success(bpnList.stream().allMatch(bpn -> Objects.equals(identity, bpn)));
}
return failure("Invalid right-value: operator '%s' requires a 'String' or a 'List' but got a '%s'".formatted(operator, Optional.of(rightValue).map(Object::getClass).map(Class::getName).orElse(null)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,32 @@
import org.eclipse.edc.spi.agent.ParticipantAgent;
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.EnumSource;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;

import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class BusinessPartnerNumberPermissionFunctionTest {

private final Permission permission = mock();
private BusinessPartnerNumberPermissionFunction validation;

private PolicyContext policyContext;
private ParticipantAgent participantAgent;

private Permission permission = mock();

@BeforeEach
void beforeEach() {
this.policyContext = mock(PolicyContext.class);
Expand All @@ -58,21 +62,18 @@ void beforeEach() {
};
}

@ParameterizedTest
@EnumSource(Operator.class)
void testFailsOnUnsupportedOperations(Operator operator) {
if (operator == Operator.EQ) { // only allowed operator
return;
}
assertFalse(validation.evaluate(operator, "foo", permission, policyContext));
verify(policyContext).reportProblem(argThat(message -> message.contains("As operator only 'EQ' is supported")));
@ParameterizedTest(name = "Illegal Operator {0}")
@ArgumentsSource(IllegalOperatorProvider.class)
void testFailsOnUnsupportedOperations(Operator illegalOperator) {
assertFalse(validation.evaluate(illegalOperator, "foo", permission, policyContext));
verify(policyContext).reportProblem(argThat(message -> message.startsWith("Operator %s is not supported.".formatted(illegalOperator))));
}

@Test
void testFailsOnUnsupportedRightValue() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertFalse(validation.evaluate(Operator.EQ, 1, permission, policyContext));
verify(policyContext).reportProblem(argThat(message -> message.contains("Invalid right operand value: expected 'String' but got")));
verify(policyContext).reportProblem(argThat(message -> message.contains("Invalid right-value: operator 'EQ' requires a 'String' or a 'List' but got a 'java.lang.Integer'")));
}

@Test
Expand All @@ -98,14 +99,99 @@ void testValidationWhenSingleParticipantIsValid() {
void testValidationFailsInvalidIdentity() {
when(participantAgent.getIdentity()).thenReturn("bar");
assertThat(validation.evaluate(Operator.EQ, "foo", permission, policyContext)).isFalse();
verify(policyContext).reportProblem(argThat(message -> message.contains("Identity of the participant not matching the expected one: foo")));
}

@Test
void testValidationForMultipleParticipants() {
when(participantAgent.getIdentity()).thenReturn("quazz");
assertThat(validation.evaluate(Operator.IN, List.of("foo", "bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IN, List.of(1, "foo"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IN, List.of("bar", "bar"), permission, policyContext)).isFalse();
}

@Test
void evaluate_neq() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.NEQ, "bar", permission, policyContext)).isTrue();

// these two should report a problem
assertThat(validation.evaluate(Operator.NEQ, 1, permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.NEQ, List.of("foo", "bar"), permission, policyContext)).isTrue();
}

assertFalse(validation.evaluate(Operator.IN, List.of("foo", "bar"), permission, policyContext));
assertFalse(validation.evaluate(Operator.IN, List.of(1, "foo"), permission, policyContext));
assertFalse(validation.evaluate(Operator.IN, List.of("bar", "bar"), permission, policyContext));
@Test
void evaluate_hasPart() {
when(participantAgent.getIdentity()).thenReturn("quizzquazz");
assertThat(validation.evaluate(Operator.HAS_PART, "quizz", permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.HAS_PART, "quazz", permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.HAS_PART, "zzqua", permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.HAS_PART, "zzqui", permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.HAS_PART, "Quizz", permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.HAS_PART, List.of("quizz"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.HAS_PART, List.of("quizz", "quazz"), permission, policyContext)).isFalse();
verify(policyContext, times(2)).reportProblem(startsWith("Invalid right-value: operator 'HAS_PART' requires a 'String' but got a"));
}

@Test
void evaluate_in() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.IN, List.of("foo", "bar"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IN, List.of("foo"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IN, List.of("bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IN, "bar", permission, policyContext)).isFalse();
verify(policyContext).reportProblem("Invalid right-value: operator 'IN' requires a 'List' but got a 'java.lang.String'");
}

@Test
void evaluate_isAnyOf() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.IS_ANY_OF, List.of("foo", "bar"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_ANY_OF, List.of("foo"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_ANY_OF, List.of("bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_ANY_OF, "bar", permission, policyContext)).isFalse();
verify(policyContext).reportProblem("Invalid right-value: operator 'IS_ANY_OF' requires a 'List' but got a 'java.lang.String'");

}

@Test
void evaluate_isA() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.IS_A, List.of("foo", "bar"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_A, List.of("foo"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_A, List.of("bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_A, "bar", permission, policyContext)).isFalse();
verify(policyContext).reportProblem("Invalid right-value: operator 'IS_A' requires a 'List' but got a 'java.lang.String'");

}

@Test
void evaluate_isAllOf() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.IS_ALL_OF, List.of("foo", "bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_ALL_OF, List.of("foo"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_ALL_OF, List.of("bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_ALL_OF, "bar", permission, policyContext)).isFalse();
}

@Test
void evaluate_isNoneOf() {
when(participantAgent.getIdentity()).thenReturn("foo");
assertThat(validation.evaluate(Operator.IS_NONE_OF, List.of("foo", "bar"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_NONE_OF, List.of("foo"), permission, policyContext)).isFalse();
assertThat(validation.evaluate(Operator.IS_NONE_OF, List.of("bar"), permission, policyContext)).isTrue();
assertThat(validation.evaluate(Operator.IS_NONE_OF, "bar", permission, policyContext)).isFalse();
verify(policyContext).reportProblem("Invalid right-value: operator 'IS_NONE_OF' requires a 'List' but got a 'java.lang.String'");
}

private static class IllegalOperatorProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
return Stream.of(
Arguments.of(Operator.GEQ),
Arguments.of(Operator.GT),
Arguments.of(Operator.LEQ),
Arguments.of(Operator.LT)
);
}
}
}

0 comments on commit 32344e2

Please sign in to comment.