All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.security.ams.dcn.engine.Engine Maven / Gradle / Ivy

Go to download

Client Library for integrating Jakarta EE applications with SAP Authorization Management Service (AMS)

The newest version!
/************************************************************************
 * © 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 ************************************************************************/
package com.sap.cloud.security.ams.dcn.engine;

import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.AND;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.BETWEEN;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.EQ;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.GE;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.GT;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.IN;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.IS_NOT_NULL;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.IS_NULL;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.LE;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.LIKE;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.LT;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.NE;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.NOT_BETWEEN;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.NOT_IN;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.NOT_LIKE;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.NOT_RESTRICTED;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.OR;
import static com.sap.cloud.security.ams.dcl.client.el.Call.Names.RESTRICTED;
import static com.sap.cloud.security.ams.dcl.client.pdp.Attributes.Names.DCL_ACTION;
import static com.sap.cloud.security.ams.dcl.client.pdp.Attributes.Names.DCL_RESOURCE;
import static com.sap.cloud.security.ams.dcl.client.pdp.Attributes.Names.DCL_SCOPE_FILTER;
import static com.sap.cloud.security.ams.dcl.client.pdp.Attributes.Names.DCL_TENANT;

import com.sap.cloud.security.ams.dcl.client.dcn.DcnRepository;
import com.sap.cloud.security.ams.dcl.client.dcn.Function;
import com.sap.cloud.security.ams.dcl.client.dcn.Policy;
import com.sap.cloud.security.ams.dcl.client.dcn.Rule;
import com.sap.cloud.security.ams.dcl.client.el.AttributeName;
import com.sap.cloud.security.ams.dcl.client.el.Call;
import com.sap.cloud.security.ams.dcl.client.pdp.Attributes;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.ObjectUtils;

/** The DCN engine that is used to replace the open policy agent (OPA). */
public class Engine {

  private static final RestrictFunction DEFAULT_RESTRICT_FUNCTION = (name, fallback) -> fallback;
  private final DcnRepository repository;
  private final EvaluationNode evaluationTree;
  private final EvaluationNodeFactory factory;

  public Engine(DcnRepository repository, EvaluationNodeFactory factory) {
    this.repository = repository;
    this.factory = factory;
    this.evaluationTree = buildEvaluationTree().deepSimplify();
  }

  private EvaluationNode buildEvaluationTree() {
    Collection policyNodes = new LinkedList<>();
    Collection scopeFilterNodes = new LinkedList<>();
    scopeFilterNodes.add(factory.isNullNode(factory.attributeNode(DCL_SCOPE_FILTER)));

    repository
        .getPolicies()
        .forEach(
            policy -> {
              EvaluationNode policyContentNode =
                  policyContentToEvaluationNode(policy, DEFAULT_RESTRICT_FUNCTION);

              String policyName = policy.qualifiedName().toEncodedString();
              scopeFilterNodes.add(
                  factory.allOfNode(
                      factory.inNode(
                          factory.valueNode(policyName), factory.attributeNode(DCL_SCOPE_FILTER)),
                      policyContentNode));

              EvaluationNode policyNode =
                  policy.defaultPolicy()
                      ? policyContentNode
                      : factory.allOfNode(
                          factory.inNode(
                              factory.valueNode(policyName), factory.effectivePoliciesNode()),
                          policyContentNode);

              String tenant = repository.getTenant(policy);
              if (ObjectUtils.isEmpty(tenant)) {
                policyNodes.add(policyNode);
              } else {
                policyNodes.add(
                    factory.allOfNode(
                        factory.equalsNode(
                            factory.valueNode(tenant), factory.attributeNode(DCL_TENANT)),
                        policyNode));
              }
            });

    return factory.allOfNode(factory.anyOfNode(scopeFilterNodes), factory.anyOfNode(policyNodes));
  }

  private EvaluationNode policyContentToEvaluationNode(
      Policy policy, RestrictFunction restrictFunction) {
    Collection contentNodes = new LinkedList<>();
    if (ObjectUtils.isNotEmpty(policy.rules())) {
      policy
          .rules()
          .forEach(rule -> contentNodes.add(ruleToEvaluationNode(rule, restrictFunction)));
    }
    if (ObjectUtils.isNotEmpty(policy.uses())) {
      policy
          .uses()
          .forEach(
              use -> {
                Policy usedPolicy = repository.getPolicy(use.qualifiedPolicyName());
                if (ObjectUtils.isEmpty(use.restrictions())) {
                  contentNodes.add(
                      policyContentToEvaluationNode(usedPolicy, DEFAULT_RESTRICT_FUNCTION));
                } else {
                  use.restrictions()
                      .forEach(
                          restrictCalls ->
                              contentNodes.add(
                                  policyContentToEvaluationNode(
                                      usedPolicy,
                                      restrictFunctionFrom(restrictCalls, restrictFunction))));
                }
              });
    }
    return factory.anyOfNode(contentNodes);
  }

  private RestrictFunction restrictFunctionFrom(
      List restrictCalls, RestrictFunction outerRestrictFunction) {
    Map restrictionMap = createRestrictionMap(restrictCalls);
    return (name, fallback) -> {
      Call replacement = restrictionMap.get(name);
      if (replacement != null) {
        return conditionTree(replacement, outerRestrictFunction);
      }
      return outerRestrictFunction.apply(name, fallback);
    };
  }

  private Map createRestrictionMap(List restrictCalls) {
    Map restrictionMap = new HashMap<>();
    restrictCalls.forEach(
        call -> {
          call.getArguments()
              .forEach(
                  arg -> {
                    if (arg instanceof AttributeName) {
                      if (restrictionMap.put((AttributeName) arg, call) != null) {
                        throw new IllegalArgumentException(
                            "Duplicate restriction for attribute " + arg);
                      }
                    }
                  });
        });
    return restrictionMap;
  }

  private EvaluationNode ruleToEvaluationNode(Rule rule, RestrictFunction restrictFunction) {
    Collection ruleNodes = new LinkedList<>();

    Optional.ofNullable(rule.actions())
        .ifPresent(
            actions ->
                ruleNodes.add(
                    factory.inNode(factory.attributeNode(DCL_ACTION), factory.valueNode(actions))));

    Optional.ofNullable(rule.resources())
        .ifPresent(
            resources ->
                ruleNodes.add(
                    factory.inNode(
                        factory.attributeNode(DCL_RESOURCE), factory.valueNode(resources))));

    Optional.ofNullable(rule.condition())
        .ifPresent(condition -> ruleNodes.add(conditionTree(condition, restrictFunction)));

    return factory.allOfNode(ruleNodes);
  }

  private EvaluationNode conditionTree(Object condition, RestrictFunction restrictFunction) {
    if (condition instanceof Call call) {
      return switch (call.getName()) {
        case AND ->
            factory.allOfNode(
                call.getArguments().stream()
                    .map(subCondition -> conditionTree(subCondition, restrictFunction)));
        case OR ->
            factory.anyOfNode(
                call.getArguments().stream()
                    .map(subCondition -> conditionTree(subCondition, restrictFunction)));
        case IS_NULL ->
            factory.isNullNode(conditionTree(call.getArguments().get(0), restrictFunction));
        case IS_NOT_NULL ->
            factory.isNotNullNode(conditionTree(call.getArguments().get(0), restrictFunction));
        case IN ->
            factory.inNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case NOT_IN ->
            factory.notInNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case LT ->
            factory.lessThanNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case LE ->
            factory.lessThanOrEqualsNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case EQ ->
            factory.equalsNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case NE ->
            factory.notEqualsNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case GE ->
            factory.greaterThanOrEqualsNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case GT ->
            factory.greaterThanNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction));
        case BETWEEN ->
            factory.betweenNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction),
                conditionTree(call.getArguments().get(2), restrictFunction));
        case NOT_BETWEEN ->
            factory.notBetweenNode(
                conditionTree(call.getArguments().get(0), restrictFunction),
                conditionTree(call.getArguments().get(1), restrictFunction),
                conditionTree(call.getArguments().get(2), restrictFunction));
        case LIKE ->
            switch (call.getArguments().size()) {
              case 2 ->
                  factory.likeNode(
                      conditionTree(call.getArguments().get(0), restrictFunction),
                      call.getArguments().get(1).toString());
              case 3 ->
                  factory.likeNode(
                      conditionTree(call.getArguments().get(0), restrictFunction),
                      call.getArguments().get(1).toString(),
                      call.getArguments().get(2).toString());
              default ->
                  throw new IllegalArgumentException("LIKE operator requires 2 or 3 arguments");
            };
        case NOT_LIKE ->
            switch (call.getArguments().size()) {
              case 2 ->
                  factory.notLikeNode(
                      conditionTree(call.getArguments().get(0), restrictFunction),
                      call.getArguments().get(1).toString());
              case 3 ->
                  factory.notLikeNode(
                      conditionTree(call.getArguments().get(0), restrictFunction),
                      call.getArguments().get(1).toString(),
                      call.getArguments().get(2).toString());
              default ->
                  throw new IllegalArgumentException("NOT LIKE operator requires 2 or 3 arguments");
            };
        case RESTRICTED ->
            restrictFunction.apply((AttributeName) call.getArguments().get(0), ValueNode.FALSE);
        case NOT_RESTRICTED ->
            restrictFunction.apply((AttributeName) call.getArguments().get(0), ValueNode.TRUE);
        default -> {
          Function dcnFunction = repository.getFunction(call.getQualifiedName());
          if (dcnFunction != null && call.getArguments().isEmpty()) {
            yield conditionTree(dcnFunction.result(), restrictFunction);
          }
          throw new UnsupportedOperationException("Operation " + call + " not implemented.");
        }
      };
    }

    if (condition instanceof AttributeName) {
      return factory.attributeNode((AttributeName) condition);
    }

    return factory.valueNode(condition);
  }

  /**
   * Evaluates this engines evaluation tree with the given attribute assignments. This method
   * returns true iff the result is either TRUE or IGNORE. That is, if the evaluation result is
   * false or depends on anything that is not ignored (including UNSET or any unresolved
   * expressions), then this method returns false.
   *
   * @param valueAccessor the attribute assignments
   * @return true iff the evaluation result is either TRUE or IGNORE
   */
  public boolean evaluateToBool(ValueAccessor valueAccessor) {
    EvaluationNode resultNode = evaluationTree.evaluate(valueAccessor);
    return ValueNode.TRUE.equals(resultNode) || ValueNode.IGNORE.equals(resultNode);
  }

  /**
   * Evaluates this engines evaluation tree with the given attribute assignments and transforms the
   * result into a condition object, i.e. a boolean value or a call object hierarchy. This method
   * maps IGNORE to true and UNSET to false. All other evaluation results have to be unresolved
   * expressions and are transformed into a condition object before being returned.
   *
   * @param valueAccessor the attribute assignments
   * @return the evaluation result as a condition object
   */
  public Object evaluateToCondition(ValueAccessor valueAccessor) {
    Object condition = evaluationTree.evaluate(valueAccessor).toCondition();
    if (Attributes.SpecialValue.IGNORE.equals(condition)) {
      return true;
    } else if (Attributes.SpecialValue.UNSET.equals(condition)) {
      return false;
    }
    return condition;
  }

  @FunctionalInterface
  private interface RestrictFunction {
    EvaluationNode apply(AttributeName name, EvaluationNode fallback);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy