Increase code coverage
[aai/gizmo.git] / src / main / java / org / onap / crud / service / AaiResourceService.java
1 /**\r
2  * ============LICENSE_START=======================================================\r
3  * org.onap.aai\r
4  * ================================================================================\r
5  * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved.\r
6  * Copyright © 2017-2018 Amdocs\r
7  * ================================================================================\r
8  * Licensed under the Apache License, Version 2.0 (the "License");\r
9  * you may not use this file except in compliance with the License.\r
10  * You may obtain a copy of the License at\r
11  *\r
12  *       http://www.apache.org/licenses/LICENSE-2.0\r
13  *\r
14  * Unless required by applicable law or agreed to in writing, software\r
15  * distributed under the License is distributed on an "AS IS" BASIS,\r
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
17  * See the License for the specific language governing permissions and\r
18  * limitations under the License.\r
19  * ============LICENSE_END=========================================================\r
20  */\r
21 package org.onap.crud.service;\r
22 \r
23 import java.security.cert.X509Certificate;\r
24 import java.util.AbstractMap;\r
25 import java.util.Arrays;\r
26 import java.util.HashSet;\r
27 import java.util.Map;\r
28 import java.util.Map.Entry;\r
29 import java.util.Set;\r
30 import javax.security.auth.x500.X500Principal;\r
31 import javax.servlet.http.HttpServletRequest;\r
32 import javax.ws.rs.Consumes;\r
33 import javax.ws.rs.Encoded;\r
34 import javax.ws.rs.POST;\r
35 import javax.ws.rs.PUT;\r
36 import javax.ws.rs.Path;\r
37 import javax.ws.rs.PathParam;\r
38 import javax.ws.rs.Produces;\r
39 import javax.ws.rs.core.Context;\r
40 import javax.ws.rs.core.EntityTag;\r
41 import javax.ws.rs.core.HttpHeaders;\r
42 import javax.ws.rs.core.MediaType;\r
43 import javax.ws.rs.core.Response;\r
44 import javax.ws.rs.core.Response.Status;\r
45 import javax.ws.rs.core.UriInfo;\r
46 import org.apache.commons.lang3.tuple.ImmutablePair;\r
47 import org.onap.aai.cl.api.Logger;\r
48 import org.onap.aai.cl.eelf.LoggerFactory;\r
49 import org.onap.aai.exceptions.AAIException;\r
50 import org.onap.aai.serialization.db.EdgeProperty;\r
51 import org.onap.aai.serialization.db.EdgeRule;\r
52 import org.onap.aai.serialization.db.EdgeRules;\r
53 import org.onap.aai.serialization.db.EdgeType;\r
54 import org.onap.aaiauth.auth.Auth;\r
55 import org.onap.crud.exception.CrudException;\r
56 import org.onap.crud.logging.CrudServiceMsgs;\r
57 import org.onap.crud.logging.LoggingUtil;\r
58 import org.onap.crud.parser.EdgePayload;\r
59 import org.onap.crud.parser.util.EdgePayloadUtil;\r
60 import org.onap.crud.service.CrudRestService.Action;\r
61 import org.onap.crud.util.CrudServiceConstants;\r
62 import org.onap.schema.EdgeRulesLoader;\r
63 import org.slf4j.MDC;\r
64 import com.google.gson.Gson;\r
65 import com.google.gson.JsonElement;\r
66 import com.google.gson.JsonPrimitive;\r
67 \r
68 \r
69 /**\r
70  * This defines a set of REST endpoints which allow clients to create or update graph edges\r
71  * where the edge rules defined by the A&AI will be invoked to automatically populate the\r
72  * defined edge properties.\r
73  */\r
74 public class AaiResourceService {\r
75 \r
76   private String mediaType = MediaType.APPLICATION_JSON;\r
77   public static final String HTTP_PATCH_METHOD_OVERRIDE = "X-HTTP-Method-Override";\r
78 \r
79   private Auth auth;\r
80   AbstractGraphDataService graphDataService;\r
81   Gson gson = new Gson();\r
82 \r
83   private Logger logger      = LoggerFactory.getInstance().getLogger(AaiResourceService.class.getName());\r
84   private Logger auditLogger = LoggerFactory.getInstance().getAuditLogger(AaiResourceService.class.getName());\r
85 \r
86   public AaiResourceService() {}\r
87 \r
88   /**\r
89    * Creates a new instance of the AaiResourceService.\r
90    *\r
91    * @param crudGraphDataService - Service used for interacting with the graph.\r
92    *\r
93    * @throws Exception\r
94    */\r
95   public AaiResourceService(AbstractGraphDataService graphDataService) throws Exception {\r
96     this.graphDataService = graphDataService;\r
97     this.auth = new Auth(CrudServiceConstants.CRD_AUTH_FILE);\r
98   }\r
99 \r
100   /**\r
101    * Perform any one-time initialization required when starting the service.\r
102    */\r
103   public void startup() {\r
104 \r
105     if(logger.isDebugEnabled()) {\r
106       logger.debug("AaiResourceService started!");\r
107     }\r
108   }\r
109 \r
110 \r
111   /**\r
112    * Creates a new relationship in the graph, automatically populating the edge\r
113    * properties based on the A&AI edge rules.\r
114    *\r
115    * @param content - Json structure describing the relationship to create.\r
116    * @param type    - Relationship type supplied as a URI parameter.\r
117    * @param uri     - Http request uri\r
118    * @param headers - Http request headers\r
119    * @param uriInfo - Http URI info field\r
120    * @param req     - Http request structure.\r
121    *\r
122    * @return - Standard HTTP response.\r
123    */\r
124   @POST\r
125   @Path("/relationships/{type}/")\r
126   @Consumes({MediaType.APPLICATION_JSON})\r
127   @Produces({MediaType.APPLICATION_JSON})\r
128   public Response createRelationship(String content,\r
129                                      @PathParam("type") String type,\r
130                                      @PathParam("uri") @Encoded String uri,\r
131                                      @Context HttpHeaders headers,\r
132                                      @Context UriInfo uriInfo,\r
133                                      @Context HttpServletRequest req) {\r
134 \r
135     LoggingUtil.initMdcContext(req, headers);\r
136 \r
137     if(logger.isDebugEnabled()) {\r
138       logger.debug("Incoming request..." + content);\r
139     }\r
140 \r
141     Response response = null;\r
142 \r
143     if (validateRequest(req, uri, content, Action.POST, CrudServiceConstants.CRD_AUTH_POLICY_NAME)) {\r
144 \r
145       try {\r
146 \r
147         // Extract the edge payload from the request.\r
148         EdgePayload payload = EdgePayload.fromJson(content);\r
149 \r
150         // Do some basic validation on the payload.\r
151         if (payload.getProperties() == null || payload.getProperties().isJsonNull()) {\r
152           throw new CrudException("Invalid request Payload", Status.BAD_REQUEST);\r
153         }\r
154         if (payload.getId() != null) {\r
155           throw new CrudException("ID specified , use Http PUT to update Edge", Status.BAD_REQUEST);\r
156         }\r
157         if (payload.getType() != null && !payload.getType().equals(type)) {\r
158           throw new CrudException("Edge Type mismatch", Status.BAD_REQUEST);\r
159         }\r
160 \r
161         // Apply the edge rules to our edge.\r
162         payload = applyEdgeRulesToPayload(payload);\r
163 \r
164         if(logger.isDebugEnabled()) {\r
165           logger.debug("Creating AAI edge using version " + EdgeRulesLoader.getLatestSchemaVersion() );\r
166         }\r
167 \r
168         // Now, create our edge in the graph store.\r
169         ImmutablePair<EntityTag, String> result = graphDataService.addEdge(EdgeRulesLoader.getLatestSchemaVersion(), type, payload);\r
170         response = Response.status(Status.CREATED).entity(result.getValue()).tag(result.getKey()).type(mediaType).build();\r
171 \r
172       } catch (CrudException ce) {\r
173           response = Response.status(ce.getHttpStatus()).entity(ce.getMessage()).build();\r
174       } catch (Exception e) {\r
175         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
176       }\r
177     }\r
178 \r
179     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
180     return response;\r
181   }\r
182 \r
183 \r
184   /**\r
185    * Creates a new relationship in the graph, automatically populating the edge\r
186    * properties based on the A&AI edge rules.\r
187    *\r
188    * @param content - Json structure describing the relationship to create.\r
189    * @param uri     - Http request uri\r
190    * @param headers - Http request headers\r
191    * @param uriInfo - Http URI info field\r
192    * @param req     - Http request structure.\r
193    *\r
194    * @return - Standard HTTP response.\r
195    *\r
196    */\r
197   @POST\r
198   @Path("/relationships/")\r
199   @Consumes({MediaType.APPLICATION_JSON})\r
200   @Produces({MediaType.APPLICATION_JSON})\r
201   public Response createRelationship(String content,\r
202                                      @PathParam("uri") @Encoded String uri,\r
203                                      @Context HttpHeaders headers,\r
204                                      @Context UriInfo uriInfo,\r
205                                      @Context HttpServletRequest req) {\r
206 \r
207     LoggingUtil.initMdcContext(req, headers);\r
208 \r
209     logger.debug("Incoming request..." + content);\r
210     Response response = null;\r
211 \r
212     if (validateRequest(req, uri, content, Action.POST, CrudServiceConstants.CRD_AUTH_POLICY_NAME)) {\r
213 \r
214       try {\r
215 \r
216         // Extract the edge payload from the request.\r
217         EdgePayload payload = EdgePayload.fromJson(content);\r
218 \r
219         // Do some basic validation on the payload.\r
220         if (payload.getProperties() == null || payload.getProperties().isJsonNull()) {\r
221           throw new CrudException("Invalid request Payload", Status.BAD_REQUEST);\r
222         }\r
223         if (payload.getId() != null) {\r
224           throw new CrudException("ID specified , use Http PUT to update Edge", Status.BAD_REQUEST);\r
225         }\r
226         if (payload.getType() == null || payload.getType().isEmpty()) {\r
227           throw new CrudException("Missing Edge Type ", Status.BAD_REQUEST);\r
228         }\r
229 \r
230         // Apply the edge rules to our edge.\r
231         payload = applyEdgeRulesToPayload(payload);\r
232 \r
233         // Now, create our edge in the graph store.\r
234         ImmutablePair<EntityTag, String> result = graphDataService.addEdge(EdgeRulesLoader.getLatestSchemaVersion(), payload.getType(), payload);\r
235         response = Response.status(Status.CREATED).entity(result.getValue()).tag(result.getKey()).type(mediaType).build();\r
236 \r
237       } catch (CrudException ce) {\r
238         response = Response.status(ce.getHttpStatus()).entity(ce.getMessage()).build();\r
239       } catch (Exception e) {\r
240         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
241       }\r
242     } else {\r
243       response = Response.status(Status.FORBIDDEN).entity(content)\r
244           .type(MediaType.APPLICATION_JSON).build();\r
245     }\r
246 \r
247     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
248     return response;\r
249   }\r
250 \r
251 \r
252 \r
253   /**\r
254    * Upserts a relationship into the graph, automatically populating the edge properties\r
255    * based on the A&AI edge rules.  The behaviour is as follows:\r
256    * <p>\r
257    * <li>If no relationship with the supplied identifier already exists, then a new relationship\r
258    * is created with that id.<br>\r
259    * <li>If a relationship with the supplied id DOES exist, then it is replaced with the supplied\r
260    * content.\r
261    *\r
262    * @param content - Json structure describing the relationship to create.\r
263    * @param type    - Relationship type supplied as a URI parameter.\r
264    * @param id      - Edge identifier.\r
265    * @param uri     - Http request uri\r
266    * @param headers - Http request headers\r
267    * @param uriInfo - Http URI info field\r
268    * @param req     - Http request structure.\r
269    *\r
270    * @return - Standard HTTP response.\r
271    */\r
272   @PUT\r
273   @Path("/relationships/{type}/{id}")\r
274   @Consumes({MediaType.APPLICATION_JSON})\r
275   @Produces({MediaType.APPLICATION_JSON})\r
276   public Response upsertEdge(String content,\r
277                              @PathParam("type") String type,\r
278                              @PathParam("id") String id,\r
279                              @PathParam("uri") @Encoded String uri,\r
280                              @Context HttpHeaders headers,\r
281                              @Context UriInfo uriInfo,\r
282                              @Context HttpServletRequest req) {\r
283     LoggingUtil.initMdcContext(req, headers);\r
284 \r
285     logger.debug("Incoming request..." + content);\r
286     Response response = null;\r
287 \r
288     if (validateRequest(req, uri, content, Action.PUT, CrudServiceConstants.CRD_AUTH_POLICY_NAME)) {\r
289 \r
290       try {\r
291 \r
292         // Extract the edge payload from the request.\r
293         EdgePayload payload = EdgePayload.fromJson(content);\r
294 \r
295         // Do some basic validation on the payload.\r
296         if (payload.getProperties() == null || payload.getProperties().isJsonNull()) {\r
297           throw new CrudException("Invalid request Payload", Status.BAD_REQUEST);\r
298         }\r
299         if (payload.getId() != null && !payload.getId().equals(id)) {\r
300           throw new CrudException("ID Mismatch", Status.BAD_REQUEST);\r
301         }\r
302 \r
303         // Apply the edge rules to our edge.\r
304         payload = applyEdgeRulesToPayload(payload);\r
305         ImmutablePair<EntityTag, String> result;\r
306         if (headers.getRequestHeaders().getFirst(HTTP_PATCH_METHOD_OVERRIDE) != null &&\r
307             headers.getRequestHeaders().getFirst(HTTP_PATCH_METHOD_OVERRIDE).equalsIgnoreCase("PATCH")) {\r
308           result = graphDataService.patchEdge(EdgeRulesLoader.getLatestSchemaVersion(), id, type, payload);\r
309           response = Response.status(Status.OK).entity(result.getValue()).type(mediaType).tag(result.getKey()).build();\r
310         } else {\r
311           result = graphDataService.updateEdge(EdgeRulesLoader.getLatestSchemaVersion(), id, type, payload);\r
312           response = Response.status(Status.OK).entity(result.getValue()).type(mediaType).tag(result.getKey()).build();\r
313         }\r
314 \r
315       } catch (CrudException ce) {\r
316         response = Response.status(ce.getHttpStatus()).entity(ce.getMessage()).build();\r
317       } catch (Exception e) {\r
318         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
319       }\r
320 \r
321     } else {\r
322 \r
323       response = Response.status(Status.FORBIDDEN).entity(content)\r
324           .type(MediaType.APPLICATION_JSON).build();\r
325     }\r
326 \r
327     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
328     return response;\r
329   }\r
330 \r
331 \r
332   /**\r
333    * Retrieves the properties defined in the edge rules for a relationship between the\r
334    * supplied vertex types.\r
335    *\r
336    * @param sourceVertexType - Type of source vertex for the relationship.\r
337    * @param targetVertexType - Type of target vertex for the relationship.\r
338    *\r
339    * @return - The defined properties for the relationship type.\r
340    *\r
341    * @throws CrudException\r
342    */\r
343   private Map<EdgeProperty, String> getEdgeRuleProperties(String sourceVertexType, String targetVertexType) throws CrudException {\r
344 \r
345     if(logger.isDebugEnabled()) {\r
346       logger.debug("Lookup db edge rules for " + sourceVertexType + " -> " + targetVertexType);\r
347     }\r
348 \r
349     EdgeRules rules = EdgeRules.getInstance();\r
350     EdgeRule rule;\r
351     try {\r
352 \r
353       if(logger.isDebugEnabled()) {\r
354         logger.debug("Lookup by edge type TREE");\r
355       }\r
356 \r
357       // We have no way of knowing in advance whether our relationship is considered to\r
358       // be a tree or cousing relationship, so try looking it up as a tree type first.\r
359       rule = rules.getEdgeRule(EdgeType.TREE, sourceVertexType, targetVertexType);\r
360 \r
361     } catch (AAIException e) {\r
362       try {\r
363 \r
364         if(logger.isDebugEnabled()) {\r
365           logger.debug("Lookup by edge type COUSIN");\r
366         }\r
367 \r
368         // If we are here, then our lookup by 'tree' type failed, so try looking it up\r
369         // as a 'cousin' relationship.\r
370         rule = rules.getEdgeRule(EdgeType.COUSIN, sourceVertexType, targetVertexType);\r
371 \r
372       } catch (AAIException e1) {\r
373 \r
374         // If we're here then we failed to find edge rules for this relationship.  Time to\r
375         // give up...\r
376         throw new CrudException("No edge rules for " + sourceVertexType + " -> " + targetVertexType, Status.NOT_FOUND);\r
377       }\r
378     } catch (Exception e) {\r
379 \r
380       throw new CrudException("General failure getting edge rule properties - " +\r
381                               e.getMessage(), Status.INTERNAL_SERVER_ERROR);\r
382     }\r
383 \r
384     return rule.getEdgeProperties();\r
385   }\r
386 \r
387 \r
388   /**\r
389    * This method takes an inbound edge request payload, looks up the edge rules for the\r
390    * sort of relationship defined in the payload, and automatically applies the defined\r
391    * edge properties to it.\r
392    *\r
393    * @param payload - The original edge request payload\r
394    *\r
395    * @return - An updated edge request payload, with the properties defined in the edge\r
396    *           rules automatically populated.\r
397    *\r
398    * @throws CrudException\r
399    */\r
400   public EdgePayload applyEdgeRulesToPayload(EdgePayload payload) throws CrudException {\r
401 \r
402     // Extract the types for both the source and target vertices.\r
403     String srcType = EdgePayloadUtil.getVertexNodeType(payload.getSource());\r
404     String tgtType = EdgePayloadUtil.getVertexNodeType(payload.getTarget());\r
405 \r
406     // Now, get the default properties for this edge based on the edge rules definition...\r
407     Map<EdgeProperty, String> props = getEdgeRuleProperties(srcType, tgtType);\r
408 \r
409     // ...and merge them with any custom properties provided in the request.\r
410     JsonElement mergedProperties = mergeProperties(payload.getProperties(), props);\r
411     payload.setProperties(mergedProperties);\r
412 \r
413     if(logger.isDebugEnabled()) {\r
414       logger.debug("Edge properties after applying rules for '" + srcType + " -> " + tgtType + "': " + mergedProperties);\r
415     }\r
416 \r
417     return payload;\r
418   }\r
419 \r
420 \r
421   /**\r
422    * Given a set of edge properties extracted from an edge request payload and a set of properties\r
423    * taken from the db edge rules, this method merges them into one set of properties.\r
424    * <p>\r
425    * If the client has attempted to override the defined value for a property in the db edge rules\r
426    * then the request will be rejected as invalid.\r
427    *\r
428    * @param propertiesFromRequest - Set of properties from the edge request.\r
429    * @param propertyDefaults      - Set of properties from the db edge rules.\r
430    *\r
431    * @return - A merged set of properties.\r
432    *\r
433    * @throws CrudException\r
434    */\r
435   @SuppressWarnings("unchecked")\r
436   public JsonElement mergeProperties(JsonElement propertiesFromRequest, Map<EdgeProperty, String> propertyDefaults) throws CrudException {\r
437 \r
438     // Convert the properties from the edge payload into something we can\r
439     // manipulate.\r
440     Set<Map.Entry<String, JsonElement>> properties = new HashSet<Map.Entry<String, JsonElement>>();\r
441     properties.addAll(propertiesFromRequest.getAsJsonObject().entrySet());\r
442 \r
443     Set<String> propertyKeys = new HashSet<String>();\r
444     for(Map.Entry<String, JsonElement> property : properties) {\r
445       propertyKeys.add(property.getKey());\r
446     }\r
447 \r
448     // Now, merge in the properties specified in the Db Edge Rules.\r
449     for(EdgeProperty defProperty : propertyDefaults.keySet()) {\r
450 \r
451       // If the edge rules property was explicitly specified by the\r
452       // client then we will reject the request...\r
453       if(!propertyKeys.contains(defProperty.toString())) {\r
454         properties.add(new AbstractMap.SimpleEntry<String, JsonElement>(defProperty.toString(),\r
455             (new JsonPrimitive(propertyDefaults.get(defProperty)))));\r
456 \r
457       } else {\r
458         throw new CrudException("Property " + defProperty + " defined in db edge rules can not be overriden by the client.",\r
459                                 Status.BAD_REQUEST);\r
460       }\r
461     }\r
462 \r
463     Object[] propArray = properties.toArray();\r
464     StringBuilder sb = new StringBuilder();\r
465     sb.append("{");\r
466     boolean first=true;\r
467     for(int i=0; i<propArray.length; i++) {\r
468 \r
469       Map.Entry<String, JsonElement> entry = (Entry<String, JsonElement>) propArray[i];\r
470       if(!first) {\r
471         sb.append(",");\r
472       }\r
473       sb.append("\"").append(entry.getKey()).append("\"").append(":").append(entry.getValue());\r
474       first=false;\r
475     }\r
476     sb.append("}");\r
477 \r
478     // We're done.  Return the result as a JsonElement.\r
479     return gson.fromJson(sb.toString(), JsonElement.class);\r
480   }\r
481 \r
482 \r
483   /**\r
484    * Invokes authentication validation on an incoming HTTP request.\r
485    *\r
486    * @param req                    - The HTTP request.\r
487    * @param uri                    - HTTP URI\r
488    * @param content                - Payload of the HTTP request.\r
489    * @param action                 - What HTTP action is being performed (GET/PUT/POST/PATCH/DELETE)\r
490    * @param authPolicyFunctionName - Policy function being invoked.\r
491    *\r
492    * @return true  - if the request passes validation,\r
493    *         false - otherwise.\r
494    */\r
495   protected boolean validateRequest(HttpServletRequest req,\r
496                                     String uri,\r
497                                     String content,\r
498                                     Action action,\r
499                                     String authPolicyFunctionName) {\r
500     try {\r
501       String cipherSuite = (String) req.getAttribute("javax.servlet.request.cipher_suite");\r
502       String authUser = null;\r
503       if (cipherSuite != null) {\r
504 \r
505         X509Certificate[] certChain = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");\r
506         X509Certificate clientCert = certChain[0];\r
507         X500Principal subjectDn = clientCert.getSubjectX500Principal();\r
508         authUser = subjectDn.toString();\r
509       }\r
510 \r
511       return this.auth.validateRequest(authUser!=null ? authUser.toLowerCase():"", action.toString() + ":" + authPolicyFunctionName);\r
512 \r
513     } catch (Exception e) {\r
514       logResult(action, uri, e);\r
515       return false;\r
516     }\r
517   }\r
518 \r
519   protected void logResult(Action op, String uri, Exception e) {\r
520 \r
521     logger.error(CrudServiceMsgs.EXCEPTION_DURING_METHOD_CALL,\r
522                  op.toString(),\r
523                  uri, Arrays.toString(e.getStackTrace()));\r
524 \r
525     // Clear the MDC context so that no other transaction inadvertently\r
526     // uses our transaction id.\r
527     MDC.clear();\r
528   }\r
529 }\r