2 * ============LICENSE_START=======================================================
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
11 * http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
21 package org.onap.policy.guard;
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;
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;
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;
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;
50 public class PolicyGuardXacmlHelper {
52 private static final Logger logger = LoggerFactory
53 .getLogger(PolicyGuardXacmlHelper.class);
55 public PolicyGuardXacmlHelper() {
56 init(PolicyEngine.manager.getEnvironment());
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 {
66 String authorization = null;
67 String clientAuth = null;
68 String environment = null;
71 private URLEntry[] restUrls = null;
72 private int restUrlIndex = 0;
74 // REST timeout, initialized from 'pdpx.timeout' property
75 private int timeout = 20000;
78 // initialized from 'guard.disabled', but may also be set to 'true' if
79 // there is an initialization error
80 private boolean disabled = false;
82 // errors that forced 'disabled' to be set to 'true'
83 private String errorMessage = null;
85 public String callPDP(PolicyGuardXacmlRequestAttributes xacmlReq) {
89 String response = null;
92 // Build the json request
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());
101 JSONObject jsonReq = new JSONObject();
102 jsonReq.put("decisionAttributes", attributes);
103 jsonReq.put("onapName", "PDPD");
105 URLEntry urlEntry = restUrls[restUrlIndex];
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);
123 * This makes an HTTP POST call to a running PDP RESTful servlet to get a
127 * @return response from guard which contains "Permit" or "Deny"
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;
137 // Open up the connection
139 connection = (HttpURLConnection) restURL.openConnection();
140 connection.setRequestProperty("Content-Type", "application/json");
142 // Setup our method and headers
144 connection.setRequestProperty("Accept", "application/json");
145 if (authorization != null) {
146 connection.setRequestProperty("Authorization", authorization);
148 if (clientauth != null) {
149 connection.setRequestProperty("ClientAuth", clientauth);
151 if (environment != null) {
152 connection.setRequestProperty("Environment", environment);
154 connection.setConnectTimeout(timeout);
155 connection.setReadTimeout(timeout);
156 connection.setRequestMethod("POST");
157 connection.setUseCaches(false);
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.
163 // So we need to handle this ourselves.
165 connection.setInstanceFollowRedirects(false);
166 connection.setDoOutput(true);
167 connection.setDoInput(true);
171 try (OutputStream os = connection.getOutputStream()) {
172 IOUtils.copy(is, os);
177 connection.connect();
178 if (connection.getResponseCode() == 200) {
182 ContentType contentType = null;
184 contentType = ContentType
185 .parse(connection.getContentType());
187 if (contentType.getMimeType().equalsIgnoreCase(
188 ContentType.APPLICATION_JSON.getMimeType())) {
189 InputStream iStream = connection.getInputStream();
190 int contentLength = connection.getContentLength();
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
198 if (offset == contentLength) {
199 // all expected bytes have been read
200 response = new String(buf);
203 int size = iStream.read(buf, offset, buf.length
206 if (contentLength > 0) {
207 logger.error("partial input stream");
209 // chunked response --
210 // dropped connection is expected
211 response = new String(buf, 0, offset);
218 logger.error("unknown content-type: " + contentType);
221 } catch (Exception e) {
222 String message = "Parsing Content-Type: "
223 + connection.getContentType();
224 logger.error(message, e);
228 logger.error(connection.getResponseCode() + " "
229 + connection.getResponseMessage());
231 } catch (Exception e) {
233 "Exception in 'PolicyGuardXacmlHelper.callRESTfulPDP'", e);
237 // Connection may have failed or not been 200 OK, return Indeterminate
239 if(response == null || response.isEmpty()){
240 return Util.INDETERMINATE;
243 rawDecision = new JSONObject(response).getString("decision");
248 public static PolicyGuardResponse ParseXacmlPdpResponse(
249 com.att.research.xacml.api.Response xacmlResponse) {
251 if (xacmlResponse == null) {
254 // In case the actual XACML response was null, create an empty
255 // response object with decision "Indeterminate"
257 return new PolicyGuardResponse("Indeterminate", null, "");
260 Iterator<Result> it_res = xacmlResponse.getResults().iterator();
262 Result res = it_res.next();
263 String decision_from_xacml_response = res.getDecision().toString();
264 Iterator<AttributeCategory> it_attr_cat = res.getAttributes()
266 UUID req_id_from_xacml_response = null;
267 String operation_from_xacml_response = "";
269 while (it_attr_cat.hasNext()) {
270 Iterator<Attribute> it_attr = it_attr_cat.next().getAttributes()
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());
281 if ("urn:oasis:names:tc:xacml:1.0:operation:operation-id"
283 Iterator<AttributeValue<?>> it_values = current_attr
284 .getValues().iterator();
285 operation_from_xacml_response = it_values.next().getValue()
292 return new PolicyGuardResponse(decision_from_xacml_response,
293 req_id_from_xacml_response, operation_from_xacml_response);
297 private void init(Properties properties) {
298 // used to store error messages
299 StringBuilder sb = new StringBuilder();
301 // fetch these parameters, if they exist
302 String timeoutString = properties.getProperty("pdpx.timeout");
303 String disabledString = properties.getProperty("guard.disabled");
305 if (disabledString != null) {
306 // decode optional 'guard.disabled' parameter
307 disabled = new Boolean(disabledString);
309 // skip everything else
315 * Decode 'pdpx.*' parameters
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");
329 // now, see which numeric entries (1-9) exist
330 ArrayList<URLEntry> entries = new ArrayList<>();
332 for (int index = 0; index < 10; index += 1) {
333 String urlPrefix = "guard.";
334 String pdpxPrefix = "pdpx.";
336 urlPrefix = urlPrefix + index + ".";
339 // see if the associated URL exists
340 String restURLlist = properties.getProperty(urlPrefix + "url");
341 if (nullOrEmpty(restURLlist)) {
342 // no entry for this index
346 // support a list of entries separated by semicolons. Each entry
351 for (String restURL : restURLlist.split("\\s*;\\s*")) {
352 String[] segments = restURL.split("\\s*,\\s*");
354 String password = null;
356 if (segments.length >= 2) {
357 // user id is provided
358 restURL = segments[0];
360 if (segments.length >= 3) {
361 // password is also provided
362 password = segments[2];
366 // URL does exist -- create the entry
367 URLEntry urlEntry = new URLEntry();
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)
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);
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",
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());
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)
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;
417 // include this URLEntry in the list
418 entries.add(urlEntry);
422 if (entries.size() == 0) {
423 sb.append("'pdpx.*' -- no URLs specified, ");
425 restUrls = entries.toArray(new URLEntry[0]);
428 if (timeoutString != null) {
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());
439 // if there are any errors, update 'errorMessage' & disable guard
441 if (sb.length() != 0) {
442 // remove the terminating ", ", and extract resulting error message
443 sb.setLength(sb.length() - 2);
444 errorMessage = sb.toString();
446 logger.error("Initialization failure: " + errorMessage);
451 * Check if a string is null or an empty string
454 * the string to be tested
455 * @return 'true' if the string is 'null' or has a length of 0, 'false'
458 static private boolean nullOrEmpty(String value) {
459 return (value == null || value.isEmpty());