Update snapshot and/or references of policy/xacml-pdp to latest snapshots
[policy/xacml-pdp.git] / applications / common / src / main / java / org / onap / policy / pdp / xacml / application / common / std / StdMatchableTranslator.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * ONAP
4  * ================================================================================
5  * Copyright (C) 2019-2021 AT&T Intellectual Property. All rights reserved.
6  * Modifications Copyright (C) 2021 Nordix Foundation.
7  * ================================================================================
8  * Licensed under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing, software
15  * distributed under the License is distributed on an "AS IS" BASIS,
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * See the License for the specific language governing permissions and
18  * limitations under the License.
19  *
20  * SPDX-License-Identifier: Apache-2.0
21  * ============LICENSE_END=========================================================
22  */
23
24 package org.onap.policy.pdp.xacml.application.common.std;
25
26 import com.att.research.xacml.api.Advice;
27 import com.att.research.xacml.api.Identifier;
28 import com.att.research.xacml.api.Obligation;
29 import com.att.research.xacml.api.Request;
30 import com.att.research.xacml.api.XACML3;
31 import com.att.research.xacml.std.IdentifierImpl;
32 import com.att.research.xacml.util.XACMLPolicyWriter;
33 import java.io.ByteArrayOutputStream;
34 import java.io.IOException;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Files;
37 import java.nio.file.Path;
38 import java.nio.file.Paths;
39 import java.util.Collection;
40 import java.util.HashMap;
41 import java.util.LinkedHashMap;
42 import java.util.LinkedList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Map.Entry;
46 import lombok.NoArgsConstructor;
47 import lombok.Setter;
48 import oasis.names.tc.xacml._3_0.core.schema.wd_17.AllOfType;
49 import oasis.names.tc.xacml._3_0.core.schema.wd_17.AnyOfType;
50 import oasis.names.tc.xacml._3_0.core.schema.wd_17.EffectType;
51 import oasis.names.tc.xacml._3_0.core.schema.wd_17.PolicyType;
52 import oasis.names.tc.xacml._3_0.core.schema.wd_17.RuleType;
53 import oasis.names.tc.xacml._3_0.core.schema.wd_17.TargetType;
54 import org.apache.commons.lang3.tuple.Pair;
55 import org.onap.policy.common.endpoints.http.client.HttpClient;
56 import org.onap.policy.common.utils.coder.CoderException;
57 import org.onap.policy.common.utils.coder.StandardCoder;
58 import org.onap.policy.common.utils.coder.StandardYamlCoder;
59 import org.onap.policy.models.decisions.concepts.DecisionRequest;
60 import org.onap.policy.models.decisions.concepts.DecisionResponse;
61 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
62 import org.onap.policy.models.tosca.authorative.concepts.ToscaDataType;
63 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
64 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyType;
65 import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
66 import org.onap.policy.models.tosca.simple.concepts.JpaToscaServiceTemplate;
67 import org.onap.policy.pdp.xacml.application.common.OnapObligation;
68 import org.onap.policy.pdp.xacml.application.common.PolicyApiCaller;
69 import org.onap.policy.pdp.xacml.application.common.PolicyApiException;
70 import org.onap.policy.pdp.xacml.application.common.ToscaDictionary;
71 import org.onap.policy.pdp.xacml.application.common.ToscaPolicyConversionException;
72 import org.onap.policy.pdp.xacml.application.common.ToscaPolicyTranslatorUtils;
73 import org.onap.policy.pdp.xacml.application.common.XacmlApplicationException;
74 import org.onap.policy.pdp.xacml.application.common.matchable.MatchableCallback;
75 import org.onap.policy.pdp.xacml.application.common.matchable.MatchablePolicyType;
76 import org.onap.policy.pdp.xacml.application.common.matchable.MatchableProperty;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80 /**
81  * This standard matchable translator uses Policy Types that contain "matchable" field in order
82  * to translate policies.
83  *
84  * @author pameladragosh
85  *
86  */
87 @NoArgsConstructor
88 public class StdMatchableTranslator  extends StdBaseTranslator implements MatchableCallback {
89
90     private static final Logger LOGGER = LoggerFactory.getLogger(StdMatchableTranslator.class);
91     private static final StandardYamlCoder standardYamlCoder = new StandardYamlCoder();
92
93     private final Map<ToscaConceptIdentifier, ToscaServiceTemplate> matchablePolicyTypes = new HashMap<>();
94     private final Map<ToscaConceptIdentifier, MatchablePolicyType> matchableCache = new HashMap<>();
95
96     @Setter
97     private HttpClient apiClient;
98     @Setter
99     private Path pathForData;
100
101     @Override
102     public Request convertRequest(DecisionRequest request) throws ToscaPolicyConversionException {
103         LOGGER.info("Converting Request {}", request);
104         try {
105             return StdMatchablePolicyRequest.createInstance(request);
106         } catch (XacmlApplicationException e) {
107             throw new ToscaPolicyConversionException("Failed to convert DecisionRequest", e);
108         }
109     }
110
111     /**
112      * scanObligations - scans the list of obligations and make appropriate method calls to process
113      * obligations.
114      *
115      * @param obligations Collection of obligation objects
116      * @param decisionResponse DecisionResponse object used to store any results from obligations.
117      */
118     @Override
119     protected void scanObligations(Collection<Obligation> obligations, DecisionResponse decisionResponse) {
120         //
121         // Implementing a crude "closest match" on the results, which means we will strip out
122         // any policies that has the lower weight than any of the others.
123         //
124         // Most likely these are "default" policies with a weight of zero, but not always.
125         //
126         // It is possible to have multiple policies with an equal weight, that is desired.
127         //
128         // So we need to track each policy type separately and the weights for each policy.
129         //
130         // policy-type -> weight -> List({policy-id, policy-content}, {policy-id, policy-content})
131         //
132         Map<String, Map<Integer, List<Pair<String, Map<String, Object>>>>> closestMatches = new LinkedHashMap<>();
133         //
134         // Now scan the list of obligations
135         //
136         for (Obligation obligation : obligations) {
137             Identifier obligationId = obligation.getId();
138             LOGGER.info("Obligation: {}", obligationId);
139             if (ToscaDictionary.ID_OBLIGATION_REST_BODY.equals(obligationId)) {
140                 scanClosestMatchObligation(closestMatches, obligation);
141             } else {
142                 LOGGER.warn("Unsupported Obligation Id {}", obligation.getId());
143             }
144         }
145         //
146         // Now add all the policies to the DecisionResponse
147         //
148         closestMatches.forEach((thePolicyType, weightMap) ->
149             weightMap.forEach((weight, policies) ->
150                 policies.forEach(policy -> {
151                     LOGGER.info("Policy {}", policy);
152                     decisionResponse.getPolicies().put(policy.getLeft(), policy.getRight());
153                 })
154             )
155         );
156     }
157
158     @Override
159     protected void scanAdvice(Collection<Advice> advice, DecisionResponse decisionResponse) {
160         LOGGER.warn("scanAdvice not supported by {}", this.getClass());
161     }
162
163     /**
164      * scanClosestMatchObligation - scans for the obligation specifically holding policy
165      * contents and their details.
166      *
167      * @param closestMatches Map holding the current set of highest weight policy types
168      * @param obligation Obligation object
169      */
170     protected void scanClosestMatchObligation(
171             Map<String, Map<Integer, List<Pair<String, Map<String, Object>>>>> closestMatches, Obligation obligation) {
172         //
173         // Create our OnapObligation object
174         //
175         var onapObligation = new OnapObligation(obligation);
176         //
177         // All 4 *should* be there
178         //
179         if (onapObligation.getPolicyId() == null || onapObligation.getPolicyContent() == null
180                 || onapObligation.getPolicyType() == null || onapObligation.getWeight() == null) {
181             LOGGER.error("Missing an expected attribute in obligation.");
182             return;
183         }
184         //
185         // Save the values
186         //
187         String policyId = onapObligation.getPolicyId();
188         String policyType = onapObligation.getPolicyType();
189         Map<String, Object> policyContent = onapObligation.getPolicyContentAsMap();
190         int policyWeight = onapObligation.getWeight();
191         //
192         // If the Policy Type exists, get the weight map.
193         //
194         Map<Integer, List<Pair<String, Map<String, Object>>>> weightMap = closestMatches.get(policyType);
195         if (weightMap != null) {
196             //
197             // Only need to check first one - as we will ensure there is only one weight
198             //
199             Entry<Integer, List<Pair<String, Map<String, Object>>>> firstEntry =
200                     weightMap.entrySet().iterator().next();
201             if (policyWeight < firstEntry.getKey()) {
202                 //
203                 // Existing policies have a greater weight, so we will not add it
204                 //
205                 LOGGER.info("{} is lesser weight {} than current policies, will not return it", policyWeight,
206                         firstEntry.getKey());
207             } else if (firstEntry.getKey().equals(policyWeight)) {
208                 //
209                 // Same weight - we will add it
210                 //
211                 LOGGER.info("Same weight {}, adding policy", policyWeight);
212                 firstEntry.getValue().add(Pair.of(policyId, policyContent));
213             } else {
214                 //
215                 // The weight is greater, so we need to remove the other policies
216                 // and point to this one.
217                 //
218                 LOGGER.info("New policy has greater weight {}, replacing {}", policyWeight, firstEntry.getKey());
219                 List<Pair<String, Map<String, Object>>> listPolicies = new LinkedList<>();
220                 listPolicies.add(Pair.of(policyId, policyContent));
221                 weightMap.clear();
222                 weightMap.put(policyWeight, listPolicies);
223             }
224         } else {
225             //
226             // Create a new entry
227             //
228             LOGGER.info("New entry {} weight {}", policyType, policyWeight);
229             List<Pair<String, Map<String, Object>>> listPolicies = new LinkedList<>();
230             listPolicies.add(Pair.of(policyId, policyContent));
231             Map<Integer, List<Pair<String, Map<String, Object>>>> newWeightMap = new LinkedHashMap<>();
232             newWeightMap.put(policyWeight, listPolicies);
233             closestMatches.put(policyType, newWeightMap);
234         }
235     }
236
237     @Override
238     public Object convertPolicy(ToscaPolicy toscaPolicy) throws ToscaPolicyConversionException {
239         //
240         // Get the TOSCA Policy Type for this policy
241         //
242         ToscaServiceTemplate toscaPolicyTypeTemplate = this.findPolicyType(toscaPolicy.getTypeIdentifier());
243         //
244         // If we don't have any TOSCA policy types, then we cannot know
245         // which properties are matchable.
246         //
247         if (toscaPolicyTypeTemplate == null) {
248             throw new ToscaPolicyConversionException(
249                     "Cannot retrieve Policy Type definition for policy " + toscaPolicy.getName());
250         }
251         //
252         // Policy name should be at the root
253         //
254         String policyName = String.valueOf(toscaPolicy.getMetadata().get(POLICY_ID));
255         //
256         // Set it as the policy ID
257         //
258         var newPolicyType = new PolicyType();
259         newPolicyType.setPolicyId(policyName);
260         //
261         // Optional description
262         //
263         newPolicyType.setDescription(toscaPolicy.getDescription());
264         //
265         // There should be a metadata section
266         //
267         fillMetadataSection(newPolicyType, toscaPolicy.getMetadata());
268         //
269         // Set the combining rule
270         //
271         newPolicyType.setRuleCombiningAlgId(XACML3.ID_RULE_FIRST_APPLICABLE.stringValue());
272         //
273         // Generate the TargetType - the policy should not be evaluated
274         // unless all the matchable properties it cares about are matched.
275         //
276         Pair<TargetType, Integer> pairGenerated = generateTargetType(toscaPolicy, toscaPolicyTypeTemplate);
277         newPolicyType.setTarget(pairGenerated.getLeft());
278         //
279         // Now represent the policy as Json
280         //
281         var coder = new StandardCoder();
282         String jsonPolicy;
283         try {
284             jsonPolicy = coder.encode(toscaPolicy);
285         } catch (CoderException e) {
286             throw new ToscaPolicyConversionException("Failed to encode policy to json", e);
287         }
288         //
289         // Add it as an obligation
290         //
291         addObligation(newPolicyType, policyName, jsonPolicy, pairGenerated.getRight(), toscaPolicy.getType());
292         //
293         // Now create the Permit Rule.
294         //
295         var rule = new RuleType();
296         rule.setDescription("Default is to PERMIT if the policy matches.");
297         rule.setRuleId(policyName + ":rule");
298         rule.setEffect(EffectType.PERMIT);
299         rule.setTarget(new TargetType());
300         //
301         // The rule contains the Condition which adds logic for
302         // optional policy-type filtering.
303         //
304         rule.setCondition(generateConditionForPolicyType(toscaPolicy.getType()));
305         //
306         // Add the rule to the policy
307         //
308         newPolicyType.getCombinerParametersOrRuleCombinerParametersOrVariableDefinition().add(rule);
309         //
310         // Log output of the policy
311         //
312         try (var os = new ByteArrayOutputStream()) {
313             XACMLPolicyWriter.writePolicyFile(os, newPolicyType);
314             LOGGER.info("{}", os);
315         } catch (IOException e) {
316             LOGGER.error("Failed to create byte array stream", e);
317         }
318         //
319         // Done
320         //
321         return newPolicyType;
322     }
323
324     @Override
325     public ToscaPolicyType retrievePolicyType(String derivedFrom) {
326         ToscaServiceTemplate template = this.findPolicyType(new ToscaConceptIdentifier(derivedFrom, "1.0.0"));
327         if (template == null) {
328             LOGGER.error("Could not retrieve Policy Type {}", derivedFrom);
329             return null;
330         }
331         return template.getPolicyTypes().get(derivedFrom);
332     }
333
334     @Override
335     public ToscaDataType retrieveDataType(String datatype) {
336         //
337         // Our outer class is not storing the current template being scanned
338         //
339         LOGGER.error("this retrieveDataType should not be called.");
340         return null;
341     }
342
343     private class MyMatchableCallback implements MatchableCallback {
344         private StdMatchableTranslator translator;
345         private ToscaServiceTemplate template;
346
347         public MyMatchableCallback(StdMatchableTranslator translator, ToscaServiceTemplate template) {
348             this.translator = translator;
349             this.template = template;
350         }
351
352         @Override
353         public ToscaPolicyType retrievePolicyType(String derivedFrom) {
354             ToscaPolicyType policyType = this.template.getPolicyTypes().get(derivedFrom);
355             if (policyType != null) {
356                 return policyType;
357             }
358             return translator.retrievePolicyType(derivedFrom);
359         }
360
361         @Override
362         public ToscaDataType retrieveDataType(String datatype) {
363             return this.template.getDataTypes().get(datatype);
364         }
365
366     }
367
368     /**
369      * For generating target type, we scan for matchable properties
370      * and use those to build the policy.
371      *
372      * @param policy the policy
373      * @param template template containing the policy
374      * @return {@code Pair<TargetType, Integer>} Returns a TargetType and a Total Weight of matchables.
375      */
376     protected Pair<TargetType, Integer> generateTargetType(ToscaPolicy policy, ToscaServiceTemplate template) {
377         //
378         // Our return object
379         //
380         var target = new TargetType();
381         //
382         // See if we have a matchable in the cache already
383         //
384         var matchablePolicyType = matchableCache.get(policy.getTypeIdentifier());
385         //
386         // If not found, create one
387         //
388         if (matchablePolicyType == null) {
389             //
390             // Our callback
391             //
392             var myCallback = new MyMatchableCallback(this, template);
393             //
394             // Create the matchable
395             //
396             matchablePolicyType = new MatchablePolicyType(
397                     template.getPolicyTypes().get(policy.getType()), myCallback);
398             //
399             // Cache it
400             //
401             matchableCache.put(policy.getTypeIdentifier(), matchablePolicyType);
402         }
403         //
404         // Fill in the target type with potential matchables
405         //
406         try {
407             fillTargetTypeWithMatchables(target, matchablePolicyType, policy.getProperties());
408         } catch (ToscaPolicyConversionException e) {
409             LOGGER.error("Could not generate target type", e);
410         }
411         //
412         // There may be a case for default policies there is no weight - need to clean
413         // up the target then else PDP will report bad policy missing AnyOf
414         //
415         int weight = calculateWeight(target);
416         LOGGER.debug("Weight is {} for policy {}", weight, policy.getName());
417         //
418         // Assume the number of AllOf's is the weight for now
419         //
420         return Pair.of(target, weight);
421     }
422
423     @SuppressWarnings("unchecked")
424     protected void fillTargetTypeWithMatchables(TargetType target, MatchablePolicyType matchablePolicyType,
425             Map<String, Object> properties) throws ToscaPolicyConversionException {
426         for (Entry<String, Object> entrySet : properties.entrySet()) {
427             String propertyName = entrySet.getKey();
428             Object propertyValue = entrySet.getValue();
429             MatchableProperty matchable = matchablePolicyType.get(propertyName);
430             if (matchable != null) {
431                 //
432                 // Construct attribute id
433                 //
434                 Identifier id = new IdentifierImpl(ToscaDictionary.ID_RESOURCE_MATCHABLE + propertyName);
435                 //
436                 // Depending on what type it is, add it into the target
437                 //
438                 ToscaPolicyTranslatorUtils.buildAndAppendTarget(target,
439                         matchable.getType().generate(propertyValue, id));
440
441                 continue;
442             }
443             //
444             // Here is the special case where we look for a Collection of values that may
445             // contain potential matchables
446             //
447             if (propertyValue instanceof List) {
448                 for (Object listValue : ((List<?>) propertyValue)) {
449                     if (listValue instanceof Map) {
450                         fillTargetTypeWithMatchables(target, matchablePolicyType, (Map<String, Object>) listValue);
451                     }
452                 }
453             } else if (propertyValue instanceof Map) {
454                 fillTargetTypeWithMatchables(target, matchablePolicyType, (Map<String, Object>) propertyValue);
455             }
456         }
457     }
458
459     protected int calculateWeight(TargetType target) {
460         var weight = 0;
461         for (AnyOfType anyOf : target.getAnyOf()) {
462             for (AllOfType allOf : anyOf.getAllOf()) {
463                 weight += allOf.getMatch().size();
464             }
465         }
466
467         return weight;
468     }
469
470     /**
471      * findPolicyType - given the ToscaConceptIdentifier, finds it in memory, or
472      * then tries to find it either locally on disk or pull it from the Policy
473      * Lifecycle API the given TOSCA Policy Type.
474      *
475      * @param policyTypeId ToscaConceptIdentifier to find
476      * @return ToscaPolicyType object. Can be null if failure.
477      */
478     protected ToscaServiceTemplate findPolicyType(ToscaConceptIdentifier policyTypeId) {
479         //
480         // Is it loaded in memory?
481         //
482         ToscaServiceTemplate policyTemplate = this.matchablePolicyTypes.get(policyTypeId);
483         if (policyTemplate == null)  {
484             //
485             // Load the policy
486             //
487             policyTemplate = this.loadPolicyType(policyTypeId);
488             //
489             // Save it
490             //
491             if (policyTemplate != null) {
492                 this.matchablePolicyTypes.put(policyTypeId, policyTemplate);
493             }
494         }
495         //
496         // Yep return it
497         //
498         return policyTemplate;
499     }
500
501     /**
502      * loadPolicyType - Tries to load the given ToscaConceptIdentifier from local
503      * storage. If it does not exist, will then attempt to pull from Policy Lifecycle
504      * API.
505      *
506      * @param policyTypeId ToscaConceptIdentifier input
507      * @return ToscaPolicyType object. Null if failure.
508      */
509     protected ToscaServiceTemplate loadPolicyType(ToscaConceptIdentifier policyTypeId) {
510         //
511         // Construct what the file name should be
512         //
513         var policyTypePath = this.constructLocalFilePath(policyTypeId);
514         //
515         // See if it exists
516         //
517         byte[] bytes;
518         try {
519             //
520             // If it exists locally, read the bytes in
521             //
522             bytes = Files.readAllBytes(policyTypePath);
523         } catch (IOException e) {
524             //
525             // Does not exist locally, so let's GET it from the policy api
526             //
527             LOGGER.error("PolicyType not found in data area yet {}", policyTypePath, e);
528             //
529             // So let's pull it from API REST call and save it locally
530             //
531             return this.pullPolicyType(policyTypeId, policyTypePath);
532         }
533         //
534         // Success - we have read locally the policy type. Now bring it into our
535         // return object.
536         //
537         LOGGER.info("Read in local policy type {}", policyTypePath.toAbsolutePath());
538         try {
539             //
540             // Decode the template
541             //
542             ToscaServiceTemplate template = standardYamlCoder.decode(new String(bytes, StandardCharsets.UTF_8),
543                     ToscaServiceTemplate.class);
544             //
545             // Ensure all the fields are setup correctly
546             //
547             var jtst = new JpaToscaServiceTemplate();
548             jtst.fromAuthorative(template);
549             return jtst.toAuthorative();
550         } catch (CoderException e) {
551             LOGGER.error("Failed to decode tosca template for {}", policyTypePath, e);
552         }
553         //
554         // Hopefully we never get here
555         //
556         LOGGER.error("Failed to find/load policy type {}", policyTypeId);
557         return null;
558     }
559
560     /**
561      * pullPolicyType - pulls the given ToscaConceptIdentifier from the Policy Lifecycle API.
562      * If successful, will store it locally given the policyTypePath.
563      *
564      * @param policyTypeId ToscaConceptIdentifier
565      * @param policyTypePath Path object to store locally
566      * @return ToscaPolicyType object. Null if failure.
567      */
568     protected synchronized ToscaServiceTemplate pullPolicyType(ToscaConceptIdentifier policyTypeId,
569             Path policyTypePath) {
570         //
571         // This is what we return
572         //
573         ToscaServiceTemplate policyTemplate = null;
574         try {
575             var api = new PolicyApiCaller(this.apiClient);
576
577             policyTemplate = api.getPolicyType(policyTypeId);
578         } catch (PolicyApiException e) {
579             LOGGER.error("Failed to make API call", e);
580             return null;
581         }
582         LOGGER.info("Successfully pulled {}", policyTypeId);
583         //
584         // Store it locally
585         //
586         try {
587             standardYamlCoder.encode(policyTypePath.toFile(), policyTemplate);
588         } catch (CoderException e) {
589             LOGGER.error("Failed to store {} locally to {}", policyTypeId, policyTypePath, e);
590         }
591         //
592         // Done return the policy type
593         //
594         return policyTemplate;
595     }
596
597     /**
598      * constructLocalFilePath - common method to ensure the name of the local file for the
599      * policy type is the same.
600      *
601      * @param policyTypeId ToscaConceptIdentifier
602      * @return Path object
603      */
604     protected Path constructLocalFilePath(ToscaConceptIdentifier policyTypeId) {
605         return Paths.get(this.pathForData.toAbsolutePath().toString(), policyTypeId.getName() + "-"
606                 + policyTypeId.getVersion() + ".yaml");
607     }
608 }