Add yaml support to xacml-pdp rest server
[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 AT&T Intellectual Property. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  *
19  * SPDX-License-Identifier: Apache-2.0
20  * ============LICENSE_END=========================================================
21  */
22
23 package org.onap.policy.pdp.xacml.application.common.std;
24
25 import com.att.research.xacml.api.AttributeAssignment;
26 import com.att.research.xacml.api.DataTypeException;
27 import com.att.research.xacml.api.Decision;
28 import com.att.research.xacml.api.Identifier;
29 import com.att.research.xacml.api.Obligation;
30 import com.att.research.xacml.api.Request;
31 import com.att.research.xacml.api.Response;
32 import com.att.research.xacml.api.Result;
33 import com.att.research.xacml.api.XACML3;
34 import com.att.research.xacml.std.IdentifierImpl;
35 import com.att.research.xacml.util.XACMLPolicyWriter;
36 import com.google.gson.Gson;
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
39 import java.nio.charset.StandardCharsets;
40 import java.nio.file.Files;
41 import java.nio.file.Path;
42 import java.nio.file.Paths;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collection;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Map.Entry;
51 import lombok.Setter;
52 import oasis.names.tc.xacml._3_0.core.schema.wd_17.AnyOfType;
53 import oasis.names.tc.xacml._3_0.core.schema.wd_17.AttributeAssignmentExpressionType;
54 import oasis.names.tc.xacml._3_0.core.schema.wd_17.AttributeValueType;
55 import oasis.names.tc.xacml._3_0.core.schema.wd_17.EffectType;
56 import oasis.names.tc.xacml._3_0.core.schema.wd_17.MatchType;
57 import oasis.names.tc.xacml._3_0.core.schema.wd_17.ObjectFactory;
58 import oasis.names.tc.xacml._3_0.core.schema.wd_17.ObligationExpressionType;
59 import oasis.names.tc.xacml._3_0.core.schema.wd_17.ObligationExpressionsType;
60 import oasis.names.tc.xacml._3_0.core.schema.wd_17.PolicyType;
61 import oasis.names.tc.xacml._3_0.core.schema.wd_17.RuleType;
62 import oasis.names.tc.xacml._3_0.core.schema.wd_17.TargetType;
63 import org.onap.policy.common.endpoints.parameters.RestServerParameters;
64 import org.onap.policy.common.utils.coder.CoderException;
65 import org.onap.policy.common.utils.coder.StandardCoder;
66 import org.onap.policy.models.decisions.concepts.DecisionRequest;
67 import org.onap.policy.models.decisions.concepts.DecisionResponse;
68 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
69 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyType;
70 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicyTypeIdentifier;
71 import org.onap.policy.models.tosca.authorative.concepts.ToscaProperty;
72 import org.onap.policy.models.tosca.authorative.concepts.ToscaServiceTemplate;
73 import org.onap.policy.models.tosca.simple.concepts.JpaToscaServiceTemplate;
74 import org.onap.policy.pdp.xacml.application.common.PolicyApiCaller;
75 import org.onap.policy.pdp.xacml.application.common.PolicyApiException;
76 import org.onap.policy.pdp.xacml.application.common.ToscaDictionary;
77 import org.onap.policy.pdp.xacml.application.common.ToscaPolicyConversionException;
78 import org.onap.policy.pdp.xacml.application.common.ToscaPolicyTranslator;
79 import org.onap.policy.pdp.xacml.application.common.ToscaPolicyTranslatorUtils;
80 import org.slf4j.Logger;
81 import org.slf4j.LoggerFactory;
82
83 /**
84  * This standard matchable translator uses Policy Types that contain "matchable" field in order
85  * to translate policies.
86  *
87  * @author pameladragosh
88  *
89  */
90 public class StdMatchableTranslator implements ToscaPolicyTranslator {
91
92     private static final Logger LOGGER = LoggerFactory.getLogger(StdMatchableTranslator.class);
93     private static final String POLICY_ID = "policy-id";
94     private static final StandardCoder standardCoder = new StandardCoder();
95
96     private final Map<ToscaPolicyTypeIdentifier, ToscaPolicyType> matchablePolicyTypes = new HashMap<>();
97     @Setter
98     private RestServerParameters apiRestParameters;
99     @Setter
100     private Path pathForData;
101
102     public StdMatchableTranslator() {
103         super();
104     }
105
106     @Override
107     public Request convertRequest(DecisionRequest request) {
108         LOGGER.info("Converting Request {}", request);
109         try {
110             return StdMatchablePolicyRequest.createInstance(request);
111         } catch (IllegalArgumentException | IllegalAccessException | DataTypeException e) {
112             LOGGER.error("Failed to convert DecisionRequest: {}", e);
113         }
114         //
115         // TODO throw exception
116         //
117         return null;
118     }
119
120     @Override
121     public DecisionResponse convertResponse(Response xacmlResponse) {
122         LOGGER.info("Converting Response {}", xacmlResponse);
123         DecisionResponse decisionResponse = new DecisionResponse();
124         //
125         // Setup policies
126         //
127         decisionResponse.setPolicies(new HashMap<>());
128         //
129         // Iterate through all the results
130         //
131         for (Result xacmlResult : xacmlResponse.getResults()) {
132             //
133             // Check the result
134             //
135             if (xacmlResult.getDecision() == Decision.PERMIT) {
136                 //
137                 // Go through obligations
138                 //
139                 scanObligations(xacmlResult.getObligations(), decisionResponse);
140             }
141             if (xacmlResult.getDecision() == Decision.DENY
142                     || xacmlResult.getDecision() == Decision.INDETERMINATE) {
143                 //
144                 // TODO we have to return an ErrorResponse object instead
145                 //
146                 decisionResponse.setStatus("A better error message");
147             }
148         }
149
150         return decisionResponse;
151     }
152
153     protected void scanObligations(Collection<Obligation> obligations, DecisionResponse decisionResponse) {
154         for (Obligation obligation : obligations) {
155             LOGGER.info("Obligation: {}", obligation);
156             for (AttributeAssignment assignment : obligation.getAttributeAssignments()) {
157                 LOGGER.info("Attribute Assignment: {}", assignment);
158                 //
159                 // We care about the content attribute
160                 //
161                 if (ToscaDictionary.ID_OBLIGATION_POLICY_MONITORING_CONTENTS
162                         .equals(assignment.getAttributeId())) {
163                     //
164                     // The contents are in Json form
165                     //
166                     Object stringContents = assignment.getAttributeValue().getValue();
167                     if (LOGGER.isInfoEnabled()) {
168                         LOGGER.info("Policy contents: {}{}", System.lineSeparator(), stringContents);
169                     }
170                     //
171                     // Let's parse it into a map using Gson
172                     //
173                     Gson gson = new Gson();
174                     @SuppressWarnings("unchecked")
175                     Map<String, Object> result = gson.fromJson(stringContents.toString() ,Map.class);
176                     //
177                     // Find the metadata section
178                     //
179                     @SuppressWarnings("unchecked")
180                     Map<String, Object> metadata = (Map<String, Object>) result.get("metadata");
181                     if (metadata != null) {
182                         decisionResponse.getPolicies().put(metadata.get(POLICY_ID).toString(), result);
183                     } else {
184                         LOGGER.error("Missing metadata section in policy contained in obligation.");
185                     }
186                 }
187             }
188         }
189
190     }
191
192     @Override
193     public PolicyType convertPolicy(ToscaPolicy toscaPolicy) throws ToscaPolicyConversionException {
194         //
195         // Get the TOSCA Policy Type for this policy
196         //
197         Collection<ToscaPolicyType> policyTypes = this.getPolicyTypes(toscaPolicy.getTypeIdentifier());
198         //
199         // If we don't have any policy types, then we cannot know
200         // which properties are matchable.
201         //
202         if (policyTypes.isEmpty()) {
203             throw new ToscaPolicyConversionException(
204                     "Cannot retrieve Policy Type definition for policy " + toscaPolicy.getName());
205         }
206         //
207         // Policy name should be at the root
208         //
209         String policyName = toscaPolicy.getMetadata().get(POLICY_ID);
210         //
211         // Set it as the policy ID
212         //
213         PolicyType newPolicyType = new PolicyType();
214         newPolicyType.setPolicyId(policyName);
215         //
216         // Optional description
217         //
218         newPolicyType.setDescription(toscaPolicy.getDescription());
219         //
220         // There should be a metadata section
221         //
222         this.fillMetadataSection(newPolicyType, toscaPolicy.getMetadata());
223         //
224         // Set the combining rule
225         //
226         newPolicyType.setRuleCombiningAlgId(XACML3.ID_RULE_FIRST_APPLICABLE.stringValue());
227         //
228         // Generate the TargetType
229         //
230         newPolicyType.setTarget(generateTargetType(toscaPolicy.getProperties(), policyTypes));
231         //
232         // Now create the Permit Rule
233         // No target since the policy has a target
234         // With obligations.
235         //
236         RuleType rule = new RuleType();
237         rule.setDescription("Default is to PERMIT if the policy matches.");
238         rule.setRuleId(policyName + ":rule");
239         rule.setEffect(EffectType.PERMIT);
240         rule.setTarget(new TargetType());
241         //
242         // Now represent the policy as Json
243         //
244         StandardCoder coder = new StandardCoder();
245         String jsonPolicy;
246         try {
247             jsonPolicy = coder.encode(toscaPolicy);
248         } catch (CoderException e) {
249             throw new ToscaPolicyConversionException("Failed to encode policy to json", e);
250         }
251         addObligation(rule, jsonPolicy);
252         //
253         // Add the rule to the policy
254         //
255         newPolicyType.getCombinerParametersOrRuleCombinerParametersOrVariableDefinition().add(rule);
256         //
257         // Return our new policy
258         //
259         try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
260             XACMLPolicyWriter.writePolicyFile(os, newPolicyType);
261             LOGGER.info("{}", os);
262         } catch (IOException e) {
263             LOGGER.error("Failed to create byte array stream", e);
264         }
265         return newPolicyType;
266     }
267
268     /**
269      * From the TOSCA metadata section, pull in values that are needed into the XACML policy.
270      *
271      * @param policy Policy Object to store the metadata
272      * @param map The Metadata TOSCA Map
273      * @return Same Policy Object
274      * @throws ToscaPolicyConversionException If there is something missing from the metadata
275      */
276     protected PolicyType fillMetadataSection(PolicyType policy,
277             Map<String, String> map) throws ToscaPolicyConversionException {
278         if (! map.containsKey(POLICY_ID)) {
279             throw new ToscaPolicyConversionException(policy.getPolicyId() + " missing metadata policy-id");
280         } else {
281             //
282             // Do nothing here - the XACML PolicyId is used from TOSCA Policy Name field
283             //
284         }
285         if (! map.containsKey("policy-version")) {
286             throw new ToscaPolicyConversionException(policy.getPolicyId() + " missing metadata policy-version");
287         } else {
288             //
289             // Add in the Policy Version
290             //
291             policy.setVersion(map.get("policy-version"));
292         }
293         return policy;
294     }
295
296     /**
297      * For generating target type, we are scan for matchable properties
298      * and use those to build the policy.
299      *
300      * @param properties Properties section of policy
301      * @param policyTypes Collection of policy Type to find matchable metadata
302      * @return TargetType object
303      */
304     @SuppressWarnings("unchecked")
305     protected TargetType generateTargetType(Map<String, Object> properties, Collection<ToscaPolicyType> policyTypes) {
306         TargetType targetType = new TargetType();
307         //
308         // Iterate the properties
309         //
310         for (Entry<String, Object> entrySet : properties.entrySet()) {
311             //
312             // Find matchable properties
313             //
314             if (isMatchable(entrySet.getKey(), policyTypes)) {
315                 LOGGER.info("Found matchable property {}", entrySet.getValue());
316                 if (entrySet.getValue() instanceof Collection) {
317                     AnyOfType anyOf = generateMatches((Collection<Object>) entrySet.getValue(),
318                             new IdentifierImpl(ToscaDictionary.ID_RESOURCE_MATCHABLE + entrySet.getKey()));
319                     if (! anyOf.getAllOf().isEmpty()) {
320                         targetType.getAnyOf().add(anyOf);
321                     }
322                 } else {
323                     AnyOfType anyOf = generateMatches(Arrays.asList(entrySet.getValue()),
324                             new IdentifierImpl(ToscaDictionary.ID_RESOURCE_MATCHABLE + entrySet.getKey()));
325                     if (! anyOf.getAllOf().isEmpty()) {
326                         targetType.getAnyOf().add(anyOf);
327                     }
328                 }
329             }
330         }
331
332         return targetType;
333     }
334
335     protected boolean isMatchable(String propertyName, Collection<ToscaPolicyType> policyTypes) {
336         for (ToscaPolicyType policyType : policyTypes) {
337             for (Entry<String, ToscaProperty> propertiesEntry : policyType.getProperties().entrySet()) {
338                 if (! propertiesEntry.getKey().equals(propertyName)
339                         || propertiesEntry.getValue().getMetadata() == null) {
340                     continue;
341                 }
342                 for (Entry<String, String> entrySet : propertiesEntry.getValue().getMetadata().entrySet()) {
343                     if (entrySet.getKey().equals("matchable") && entrySet.getValue().equals("true")) {
344                         return true;
345                     }
346                 }
347             }
348         }
349         return false;
350     }
351
352     protected AnyOfType generateMatches(Collection<Object> matchables, Identifier attributeId) {
353         //
354         // This is our outer AnyOf - which is an OR
355         //
356         AnyOfType anyOf = new AnyOfType();
357         for (Object matchable : matchables) {
358             //
359             // Default to string
360             //
361             Identifier idFunction = XACML3.ID_FUNCTION_STRING_EQUAL;
362             Identifier idDatatype = XACML3.ID_DATATYPE_STRING;
363             //
364             // See if we are another datatype
365             //
366             // TODO We should add datetime support. But to do that we need
367             // probably more metadata to describe how that would be translated.
368             //
369             if (matchable instanceof Integer) {
370                 idFunction = XACML3.ID_FUNCTION_INTEGER_EQUAL;
371                 idDatatype = XACML3.ID_DATATYPE_INTEGER;
372             } else if (matchable instanceof Double) {
373                 idFunction = XACML3.ID_FUNCTION_DOUBLE_EQUAL;
374                 idDatatype = XACML3.ID_DATATYPE_DOUBLE;
375             } else if (matchable instanceof Boolean) {
376                 idFunction = XACML3.ID_FUNCTION_BOOLEAN_EQUAL;
377                 idDatatype = XACML3.ID_DATATYPE_BOOLEAN;
378             }
379             //
380             // Create a match for this
381             //
382             MatchType match = ToscaPolicyTranslatorUtils.buildMatchTypeDesignator(
383                     idFunction,
384                     matchable.toString(),
385                     idDatatype,
386                     attributeId,
387                     XACML3.ID_ATTRIBUTE_CATEGORY_RESOURCE);
388             //
389             // Now create an anyOf (OR)
390             //
391             anyOf.getAllOf().add(ToscaPolicyTranslatorUtils.buildAllOf(match));
392         }
393         return anyOf;
394     }
395
396     protected RuleType addObligation(RuleType rule, String jsonPolicy) {
397         //
398         // Convert the YAML Policy to JSON Object
399         //
400         if (LOGGER.isInfoEnabled()) {
401             LOGGER.info("JSON Optimization Policy {}{}", System.lineSeparator(), jsonPolicy);
402         }
403         //
404         // Create an AttributeValue for it
405         //
406         AttributeValueType value = new AttributeValueType();
407         value.setDataType(ToscaDictionary.ID_OBLIGATION_POLICY_MONITORING_DATATYPE.stringValue());
408         value.getContent().add(jsonPolicy);
409         //
410         // Create our AttributeAssignmentExpression where we will
411         // store the contents of the policy in JSON format.
412         //
413         AttributeAssignmentExpressionType expressionType = new AttributeAssignmentExpressionType();
414         expressionType.setAttributeId(ToscaDictionary.ID_OBLIGATION_POLICY_MONITORING_CONTENTS.stringValue());
415         ObjectFactory factory = new ObjectFactory();
416         expressionType.setExpression(factory.createAttributeValue(value));
417         //
418         // Create an ObligationExpression for it
419         //
420         ObligationExpressionType obligation = new ObligationExpressionType();
421         obligation.setFulfillOn(EffectType.PERMIT);
422         obligation.setObligationId(ToscaDictionary.ID_OBLIGATION_REST_BODY.stringValue());
423         obligation.getAttributeAssignmentExpression().add(expressionType);
424         //
425         // Now we can add it into the rule
426         //
427         ObligationExpressionsType obligations = new ObligationExpressionsType();
428         obligations.getObligationExpression().add(obligation);
429         rule.setObligationExpressions(obligations);
430         return rule;
431     }
432
433
434     /**
435      * Get Policy Type definitions. This could be previously loaded, or could be
436      * stored in application path, or may need to be pulled from the API.
437      *
438      *
439      * @param policyTypeId Policy Type Id
440      * @return A list of PolicyTypes
441      */
442     private List<ToscaPolicyType> getPolicyTypes(ToscaPolicyTypeIdentifier policyTypeId) {
443         //
444         // Create identifier from the policy
445         //
446         ToscaPolicyTypeIdentifier typeId = new ToscaPolicyTypeIdentifier(policyTypeId);
447         //
448         // Find the Policy Type
449         //
450         ToscaPolicyType policyType = findPolicyType(typeId);
451         if (policyType == null)  {
452             return Collections.emptyList();
453         }
454         //
455         // Create our return object
456         //
457         List<ToscaPolicyType> listTypes = new ArrayList<>();
458         listTypes.add(policyType);
459         //
460         // Look for parent policy types that could also contain matchable properties
461         //
462         ToscaPolicyType childPolicyType = policyType;
463         while (! childPolicyType.getDerivedFrom().startsWith("tosca.policies.Root")) {
464             //
465             // Create parent policy type id.
466             //
467             // We will have to assume the same version between child and the
468             // parent policy type it derives from.
469             //
470             // Or do we assume 1.0.0?
471             //
472             String strDerivedFrom = childPolicyType.getDerivedFrom();
473             //
474             // Hack that fixes policy/models appending 0.0.0 to the derivedFrom name
475             //
476             if (strDerivedFrom.endsWith("0.0.0")) {
477                 strDerivedFrom = strDerivedFrom.substring(0, strDerivedFrom.length() - "0.0.0".length() - 1);
478             }
479             ToscaPolicyTypeIdentifier parentId = new ToscaPolicyTypeIdentifier(strDerivedFrom, "1.0.0");
480             //
481             // Find the policy type
482             //
483             ToscaPolicyType parentPolicyType = findPolicyType(parentId);
484             if (parentPolicyType == null) {
485                 //
486                 // Probably would be best to throw an exception and
487                 // return nothing back.
488                 //
489                 // But instead we will log a warning
490                 //
491                 LOGGER.warn("Missing parent policy type - proceeding anyway {}", parentId);
492                 //
493                 // Break the loop
494                 //
495                 break;
496             }
497             //
498             // Great save it
499             //
500             listTypes.add(parentPolicyType);
501             //
502             // Move to the next parent
503             //
504             childPolicyType = parentPolicyType;
505         }
506
507
508         return listTypes;
509     }
510
511     private ToscaPolicyType findPolicyType(ToscaPolicyTypeIdentifier policyTypeId) {
512         //
513         // Is it loaded in memory?
514         //
515         ToscaPolicyType policyType = this.matchablePolicyTypes.get(policyTypeId);
516         if (policyType == null)  {
517             //
518             // Load the policy
519             //
520             policyType = this.loadPolicyType(policyTypeId);
521         }
522         //
523         // Yep return it
524         //
525         return policyType;
526     }
527
528     private ToscaPolicyType loadPolicyType(ToscaPolicyTypeIdentifier policyTypeId) {
529         //
530         // Construct what the file name should be
531         //
532         Path policyTypePath = this.constructLocalFilePath(policyTypeId);
533         //
534         // See if it exists
535         //
536         byte[] bytes;
537         try {
538             //
539             // If it exists locally, read the bytes in
540             //
541             bytes = Files.readAllBytes(policyTypePath);
542         } catch (IOException e) {
543             //
544             // Does not exist locally, so let's GET it from the policy api
545             //
546             LOGGER.error("PolicyType not found in data area yet {}", policyTypePath, e);
547             //
548             // So let's pull it from API REST call and save it locally
549             //
550             return this.pullPolicyType(policyTypeId, policyTypePath);
551         }
552         LOGGER.info("Read in local policy type {}", policyTypePath.toAbsolutePath());
553         try {
554             ToscaServiceTemplate serviceTemplate = standardCoder.decode(new String(bytes, StandardCharsets.UTF_8),
555                     ToscaServiceTemplate.class);
556             JpaToscaServiceTemplate jtst = new JpaToscaServiceTemplate();
557             jtst.fromAuthorative(serviceTemplate);
558             ToscaServiceTemplate completedJtst = jtst.toAuthorative();
559             //
560             // Search for our Policy Type, there really only should be one but
561             // this is returned as a map.
562             //
563             for ( Entry<String, ToscaPolicyType> entrySet : completedJtst.getPolicyTypes().entrySet()) {
564                 ToscaPolicyType entryPolicyType = entrySet.getValue();
565                 if (policyTypeId.getName().equals(entryPolicyType.getName())
566                         && policyTypeId.getVersion().equals(entryPolicyType.getVersion())) {
567                     LOGGER.info("Found existing local policy type {} {}", entryPolicyType.getName(),
568                             entryPolicyType.getVersion());
569                     //
570                     // Just simply return the policy type right here
571                     //
572                     return entryPolicyType;
573                 } else {
574                     LOGGER.warn("local policy type contains different name version {} {}", entryPolicyType.getName(),
575                             entryPolicyType.getVersion());
576                 }
577             }
578             //
579             // This would be an error, if the file stored does not match what its supposed to be
580             //
581             LOGGER.error("Existing policy type file does not contain right name and version");
582         } catch (CoderException e) {
583             LOGGER.error("Failed to decode tosca template for {}", policyTypePath, e);
584         }
585         //
586         // Hopefully we never get here
587         //
588         LOGGER.error("Failed to find/load policy type {}", policyTypeId);
589         return null;
590     }
591
592     private synchronized ToscaPolicyType pullPolicyType(ToscaPolicyTypeIdentifier policyTypeId, Path policyTypePath) {
593         //
594         // This is what we return
595         //
596         ToscaPolicyType policyType = null;
597         try {
598             PolicyApiCaller api = new PolicyApiCaller(this.apiRestParameters);
599
600             policyType = api.getPolicyType(policyTypeId);
601         } catch (PolicyApiException e) {
602             LOGGER.error("Failed to make API call", e);
603             LOGGER.error("parameters: {} ", this.apiRestParameters);
604             return null;
605         }
606         //
607         // Store it locally
608         //
609         try {
610             standardCoder.encode(policyTypePath.toFile(), policyType);
611         } catch (CoderException e) {
612             LOGGER.error("Failed to store {} locally to {}", policyTypeId, policyTypePath, e);
613         }
614         //
615         // Done return the policy type
616         //
617         return policyType;
618     }
619
620     private Path constructLocalFilePath(ToscaPolicyTypeIdentifier policyTypeId) {
621         return Paths.get(this.pathForData.toAbsolutePath().toString(), policyTypeId.getName() + "-"
622                 + policyTypeId.getVersion() + ".json");
623     }
624 }