Fix nullPointerException non-200 guard response.
[policy/drools-applications.git] / controlloop / common / guard / src / main / java / org / onap / policy / guard / PolicyGuardXacmlHelper.java
1 /*-
2  * ============LICENSE_START=======================================================
3  * guard
4  * ================================================================================
5  * Copyright (C) 2017 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  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.guard;
22
23 import java.io.BufferedReader;
24 import java.io.ByteArrayInputStream;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.io.Serializable;
29 import java.net.HttpURLConnection;
30 import java.net.URL;
31 import java.util.ArrayList;
32 import java.util.Base64;
33 import java.util.Iterator;
34 import java.util.Properties;
35 import java.util.UUID;
36
37 import org.apache.commons.io.IOUtils;
38 import org.apache.http.entity.ContentType;
39 import org.json.JSONObject;
40 import org.onap.policy.drools.system.PolicyEngine;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import com.att.research.xacml.api.Attribute;
45 import com.att.research.xacml.api.AttributeCategory;
46 import com.att.research.xacml.api.AttributeValue;
47 import com.att.research.xacml.api.Result;
48
49
50 public class PolicyGuardXacmlHelper {
51
52         private static final Logger logger = LoggerFactory
53                         .getLogger(PolicyGuardXacmlHelper.class);
54
55         public PolicyGuardXacmlHelper() {
56                 init(PolicyEngine.manager.getEnvironment());
57         }
58
59         // initialized from 'pdpx.url' property --
60         // Each entry in 'restUrls' contains a destination URL, and an optional
61         // 'Authorization' header entry. 'restUrlIndex' indicates the next
62         // entry to try -- after each failure, the index is advanced to the
63         // next entry (wrapping to the beginning, if needed).
64         static private class URLEntry implements Serializable {
65                 URL restURL;
66                 String authorization = null;
67                 String clientAuth = null;
68                 String environment = null;
69         };
70
71         private URLEntry[] restUrls = null;
72         private int restUrlIndex = 0;
73
74         // REST timeout, initialized from 'pdpx.timeout' property
75         private int timeout = 20000;
76
77
78         // initialized from 'guard.disabled', but may also be set to 'true' if
79         // there is an initialization error
80         private boolean disabled = false;
81
82         // errors that forced 'disabled' to be set to 'true'
83         private String errorMessage = null;
84
85         public String callPDP(PolicyGuardXacmlRequestAttributes xacmlReq) {
86                 //
87                 // Send it to the PDP
88                 //
89                 String response = null;
90
91                 //
92                 // Build the json request
93                 //
94                 JSONObject attributes = new JSONObject();
95                 attributes.put("actor", xacmlReq.getActor_id());
96                 attributes.put("recipe", xacmlReq.getOperation_id());
97                 attributes.put("target", xacmlReq.getTarget_id());
98                 if (xacmlReq.getClname_id() != null) {
99                         attributes.put("clname", xacmlReq.getClname_id());
100                 }
101                 JSONObject jsonReq = new JSONObject();
102                 jsonReq.put("decisionAttributes", attributes);
103                 jsonReq.put("onapName", "PDPD");
104
105                 URLEntry urlEntry = restUrls[restUrlIndex];
106
107                 try {
108                         //
109                         // Call RESTful PDP
110                         //
111                         response = callRESTfulPDP(new ByteArrayInputStream(jsonReq
112                                         .toString().getBytes()), urlEntry.restURL,
113                                         urlEntry.authorization, urlEntry.clientAuth,
114                                         urlEntry.environment);
115                 } catch (Exception e) {
116                         logger.error("Error in sending RESTful request: ", e);
117                 }
118
119                 return response;
120         }
121
122         /**
123          * This makes an HTTP POST call to a running PDP RESTful servlet to get a
124          * decision.
125          * 
126          * @param file
127          * @return response from guard which contains "Permit" or "Deny"
128          */
129         private String callRESTfulPDP(InputStream is, URL restURL,
130                         String authorization, String clientauth, String environment) {
131                 String response = null;
132                 String rawDecision = null;
133                 HttpURLConnection connection = null;
134                 try {
135
136                         //
137                         // Open up the connection
138                         //
139                         connection = (HttpURLConnection) restURL.openConnection();
140                         connection.setRequestProperty("Content-Type", "application/json");
141                         //
142                         // Setup our method and headers
143                         //
144                         connection.setRequestProperty("Accept", "application/json");
145                         if (authorization != null) {
146                                 connection.setRequestProperty("Authorization", authorization);
147                         }
148                         if (clientauth != null) {
149                                 connection.setRequestProperty("ClientAuth", clientauth);
150                         }
151                         if (environment != null) {
152                                 connection.setRequestProperty("Environment", environment);
153                         }
154                         connection.setConnectTimeout(timeout);
155                         connection.setReadTimeout(timeout);
156                         connection.setRequestMethod("POST");
157                         connection.setUseCaches(false);
158                         //
159                         // Adding this in. It seems the HttpUrlConnection class does NOT
160                         // properly forward our headers for POST re-direction. It does so
161                         // for a GET re-direction.
162                         //
163                         // So we need to handle this ourselves.
164                         //
165                         connection.setInstanceFollowRedirects(false);
166                         connection.setDoOutput(true);
167                         connection.setDoInput(true);
168                         //
169                         // Send the request
170                         //
171                         try (OutputStream os = connection.getOutputStream()) {
172                                 IOUtils.copy(is, os);
173                         }
174                         //
175                         // Do the connect
176                         //
177                         connection.connect();
178                         if (connection.getResponseCode() == 200) {
179                                 //
180                                 // Read the response
181                                 //
182                                 ContentType contentType = null;
183                                 try {
184                                         contentType = ContentType
185                                                         .parse(connection.getContentType());
186
187                                         if (contentType.getMimeType().equalsIgnoreCase(
188                                                         ContentType.APPLICATION_JSON.getMimeType())) {
189                                                 InputStream iStream = connection.getInputStream();
190                                                 int contentLength = connection.getContentLength();
191
192                                                 // if content length is -1, respose is chunked, and
193                                                 // TCP connection will be dropped at the end
194                                                 byte[] buf = new byte[contentLength < 0 ? 1024
195                                                                 : contentLength];
196                                                 int offset = 0;
197                                                 for (;;) {
198                                                         if (offset == contentLength) {
199                                                                 // all expected bytes have been read
200                                                                 response = new String(buf);
201                                                                 break;
202                                                         }
203                                                         int size = iStream.read(buf, offset, buf.length
204                                                                         - offset);
205                                                         if (size < 0) {
206                                                                 if (contentLength > 0) {
207                                                                         logger.error("partial input stream");
208                                                                 } else {
209                                                                         // chunked response --
210                                                                         // dropped connection is expected
211                                                                         response = new String(buf, 0, offset);
212                                                                 }
213                                                                 break;
214                                                         }
215                                                         offset += size;
216                                                 }
217                                         } else {
218                                                 logger.error("unknown content-type: " + contentType);
219                                         }
220
221                                 } catch (Exception e) {
222                                         String message = "Parsing Content-Type: "
223                                                         + connection.getContentType();
224                                         logger.error(message, e);
225                                 }
226
227                         } else {
228                                 logger.error(connection.getResponseCode() + " "
229                                                 + connection.getResponseMessage());
230                         }
231                 } catch (Exception e) {
232                         logger.error(
233                                         "Exception in 'PolicyGuardXacmlHelper.callRESTfulPDP'", e);
234                 }
235                 
236                 //
237                 // Connection may have failed or not been 200 OK, return Indeterminate
238                 //
239                 if(response == null || response.isEmpty()){
240                         return Util.INDETERMINATE;
241                 }
242                 
243                 rawDecision = new JSONObject(response).getString("decision");
244
245                 return rawDecision;
246         }
247
248         public static PolicyGuardResponse ParseXacmlPdpResponse(
249                         com.att.research.xacml.api.Response xacmlResponse) {
250
251                 if (xacmlResponse == null) {
252
253                         //
254                         // In case the actual XACML response was null, create an empty
255                         // response object with decision "Indeterminate"
256                         //
257                         return new PolicyGuardResponse("Indeterminate", null, "");
258                 }
259
260                 Iterator<Result> it_res = xacmlResponse.getResults().iterator();
261
262                 Result res = it_res.next();
263                 String decision_from_xacml_response = res.getDecision().toString();
264                 Iterator<AttributeCategory> it_attr_cat = res.getAttributes()
265                                 .iterator();
266                 UUID req_id_from_xacml_response = null;
267                 String operation_from_xacml_response = "";
268
269                 while (it_attr_cat.hasNext()) {
270                         Iterator<Attribute> it_attr = it_attr_cat.next().getAttributes()
271                                         .iterator();
272                         while (it_attr.hasNext()) {
273                                 Attribute current_attr = it_attr.next();
274                                 String s = current_attr.getAttributeId().stringValue();
275                                 if ("urn:oasis:names:tc:xacml:1.0:request:request-id".equals(s)) {
276                                         Iterator<AttributeValue<?>> it_values = current_attr
277                                                         .getValues().iterator();
278                                         req_id_from_xacml_response = UUID.fromString(it_values
279                                                         .next().getValue().toString());
280                                 }
281                                 if ("urn:oasis:names:tc:xacml:1.0:operation:operation-id"
282                                                 .equals(s)) {
283                                         Iterator<AttributeValue<?>> it_values = current_attr
284                                                         .getValues().iterator();
285                                         operation_from_xacml_response = it_values.next().getValue()
286                                                         .toString();
287                                 }
288
289                         }
290                 }
291
292                 return new PolicyGuardResponse(decision_from_xacml_response,
293                                 req_id_from_xacml_response, operation_from_xacml_response);
294
295         }
296
297         private void init(Properties properties) {
298                 // used to store error messages
299                 StringBuilder sb = new StringBuilder();
300
301                 // fetch these parameters, if they exist
302                 String timeoutString = properties.getProperty("pdpx.timeout");
303                 String disabledString = properties.getProperty("guard.disabled");
304
305                 if (disabledString != null) {
306                         // decode optional 'guard.disabled' parameter
307                         disabled = new Boolean(disabledString);
308                         if (disabled) {
309                                 // skip everything else
310                                 return;
311                         }
312                 }
313
314                 /*
315                  * Decode 'pdpx.*' parameters
316                  */
317
318                 // first, the default parameters
319                 String defaultUser = properties.getProperty("pdpx.username");
320                 String defaultPassword = properties
321                                 .getProperty("pdpx.password");
322                 String defaultClientUser = properties
323                                 .getProperty("pdpx.client.username");
324                 String defaultClientPassword = properties
325                                 .getProperty("pdpx.client.password");
326                 String defaultEnvironment = properties
327                                 .getProperty("pdpx.environment");
328
329                 // now, see which numeric entries (1-9) exist
330                 ArrayList<URLEntry> entries = new ArrayList<>();
331
332                 for (int index = 0; index < 10; index += 1) {
333                         String urlPrefix = "guard.";
334                         String pdpxPrefix = "pdpx.";
335                         if (index != 0) {
336                                 urlPrefix = urlPrefix + index + ".";
337                         }
338
339                         // see if the associated URL exists
340                         String restURLlist = properties.getProperty(urlPrefix + "url");
341                         if (nullOrEmpty(restURLlist)) {
342                                 // no entry for this index
343                                 continue;
344                         }
345
346                         // support a list of entries separated by semicolons. Each entry
347                         // can be:
348                         // URL
349                         // URL,user
350                         // URL,user,password
351                         for (String restURL : restURLlist.split("\\s*;\\s*")) {
352                                 String[] segments = restURL.split("\\s*,\\s*");
353                                 String user = null;
354                                 String password = null;
355
356                                 if (segments.length >= 2) {
357                                         // user id is provided
358                                         restURL = segments[0];
359                                         user = segments[1];
360                                         if (segments.length >= 3) {
361                                                 // password is also provided
362                                                 password = segments[2];
363                                         }
364                                 }
365
366                                 // URL does exist -- create the entry
367                                 URLEntry urlEntry = new URLEntry();
368                                 try {
369                                         urlEntry.restURL = new URL(restURL);
370                                 } catch (java.net.MalformedURLException e) {
371                                         // if we don't have a URL,
372                                         // don't bother with the rest on this one
373                                         sb.append("'").append(urlPrefix).append("url' '")
374                                                         .append(restURL).append("': ").append(e)
375                                                         .append(",");
376                                         continue;
377                                 }
378
379                                 if (nullOrEmpty(user)) {
380                                         // user id was not provided on '*.url' line --
381                                         // extract it from a separate property
382                                         user = properties.getProperty(pdpxPrefix + "username", defaultUser);
383                                 }
384                                 if (nullOrEmpty(password)) {
385                                         // password was not provided on '*.url' line --
386                                         // extract it from a separate property
387                                         password = properties.getProperty(pdpxPrefix + "password",
388                                                         defaultPassword);
389                                 }
390
391                                 // see if 'user' and 'password' entries both exist
392                                 if (!nullOrEmpty(user) && !nullOrEmpty(password)) {
393                                         urlEntry.authorization = "Basic "
394                                                         + Base64.getEncoder().encodeToString(
395                                                                         (user + ":" + password).getBytes());
396                                 }
397
398                                 // see if 'client.user' and 'client.password' entries both exist
399                                 String clientUser = properties.getProperty(pdpxPrefix
400                                                 + "client.username", defaultClientUser);
401                                 String clientPassword = properties.getProperty(pdpxPrefix
402                                                 + "client.password", defaultClientPassword);
403                                 if (!nullOrEmpty(clientUser) && !nullOrEmpty(clientPassword)) {
404                                         urlEntry.clientAuth = "Basic "
405                                                         + Base64.getEncoder().encodeToString(
406                                                                         (clientUser + ":" + clientPassword)
407                                                                                         .getBytes());
408                                 }
409
410                                 // see if there is an 'environment' entry
411                                 String environment = properties.getProperty(pdpxPrefix
412                                                 + "environment", defaultEnvironment);
413                                 if (!nullOrEmpty(environment)) {
414                                         urlEntry.environment = environment;
415                                 }
416
417                                 // include this URLEntry in the list
418                                 entries.add(urlEntry);
419                         }
420                 }
421
422                 if (entries.size() == 0) {
423                         sb.append("'pdpx.*' -- no URLs specified, ");
424                 } else {
425                         restUrls = entries.toArray(new URLEntry[0]);
426                 }
427
428                 if (timeoutString != null) {
429                         try {
430                                 // decode optional 'pdpx.timeout' parameter
431                                 timeout = Integer.valueOf(timeoutString);
432                         } catch (NumberFormatException e) {
433                                 sb.append("'pdpx.timeout': " + e + ", ");
434                                 logger.trace(e.getLocalizedMessage());
435                         }
436                 }
437
438
439                 // if there are any errors, update 'errorMessage' & disable guard
440                 // queries
441                 if (sb.length() != 0) {
442                         // remove the terminating ", ", and extract resulting error message
443                         sb.setLength(sb.length() - 2);
444                         errorMessage = sb.toString();
445                         disabled = true;
446                         logger.error("Initialization failure: " + errorMessage);
447                 }
448         }
449
450         /**
451          * Check if a string is null or an empty string
452          *
453          * @param value
454          *            the string to be tested
455          * @return 'true' if the string is 'null' or has a length of 0, 'false'
456          *         otherwise
457          */
458         static private boolean nullOrEmpty(String value) {
459                 return (value == null || value.isEmpty());
460         }
461
462 }