Merge "Fix sonar violations"
[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 e) {\r
173 \r
174         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
175       }\r
176     }\r
177 \r
178     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
179     return response;\r
180   }\r
181 \r
182 \r
183   /**\r
184    * Creates a new relationship in the graph, automatically populating the edge\r
185    * properties based on the A&AI edge rules.\r
186    *\r
187    * @param content - Json structure describing the relationship to create.\r
188    * @param uri     - Http request uri\r
189    * @param headers - Http request headers\r
190    * @param uriInfo - Http URI info field\r
191    * @param req     - Http request structure.\r
192    *\r
193    * @return - Standard HTTP response.\r
194    *\r
195    */\r
196   @POST\r
197   @Path("/relationships/")\r
198   @Consumes({MediaType.APPLICATION_JSON})\r
199   @Produces({MediaType.APPLICATION_JSON})\r
200   public Response createRelationship(String content,\r
201                                      @PathParam("uri") @Encoded String uri,\r
202                                      @Context HttpHeaders headers,\r
203                                      @Context UriInfo uriInfo,\r
204                                      @Context HttpServletRequest req) {\r
205 \r
206     LoggingUtil.initMdcContext(req, headers);\r
207 \r
208     logger.debug("Incoming request..." + content);\r
209     Response response = null;\r
210 \r
211     if (validateRequest(req, uri, content, Action.POST, CrudServiceConstants.CRD_AUTH_POLICY_NAME)) {\r
212 \r
213       try {\r
214 \r
215         // Extract the edge payload from the request.\r
216         EdgePayload payload = EdgePayload.fromJson(content);\r
217 \r
218         // Do some basic validation on the payload.\r
219         if (payload.getProperties() == null || payload.getProperties().isJsonNull()) {\r
220           throw new CrudException("Invalid request Payload", Status.BAD_REQUEST);\r
221         }\r
222         if (payload.getId() != null) {\r
223           throw new CrudException("ID specified , use Http PUT to update Edge", Status.BAD_REQUEST);\r
224         }\r
225         if (payload.getType() == null || payload.getType().isEmpty()) {\r
226           throw new CrudException("Missing Edge Type ", Status.BAD_REQUEST);\r
227         }\r
228 \r
229         // Apply the edge rules to our edge.\r
230         payload = applyEdgeRulesToPayload(payload);\r
231 \r
232         // Now, create our edge in the graph store.\r
233         ImmutablePair<EntityTag, String> result = graphDataService.addEdge(EdgeRulesLoader.getLatestSchemaVersion(), payload.getType(), payload);\r
234         response = Response.status(Status.CREATED).entity(result.getValue()).tag(result.getKey()).type(mediaType).build();\r
235 \r
236       } catch (CrudException ce) {\r
237         response = Response.status(ce.getHttpStatus()).entity(ce.getMessage()).build();\r
238       } catch (Exception e) {\r
239         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
240       }\r
241     } else {\r
242       response = Response.status(Status.FORBIDDEN).entity(content)\r
243           .type(MediaType.APPLICATION_JSON).build();\r
244     }\r
245 \r
246     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
247     return response;\r
248   }\r
249 \r
250 \r
251 \r
252   /**\r
253    * Upserts a relationship into the graph, automatically populating the edge properties\r
254    * based on the A&AI edge rules.  The behaviour is as follows:\r
255    * <p>\r
256    * <li>If no relationship with the supplied identifier already exists, then a new relationship\r
257    * is created with that id.<br>\r
258    * <li>If a relationship with the supplied id DOES exist, then it is replaced with the supplied\r
259    * content.\r
260    *\r
261    * @param content - Json structure describing the relationship to create.\r
262    * @param type    - Relationship type supplied as a URI parameter.\r
263    * @param id      - Edge identifier.\r
264    * @param uri     - Http request uri\r
265    * @param headers - Http request headers\r
266    * @param uriInfo - Http URI info field\r
267    * @param req     - Http request structure.\r
268    *\r
269    * @return - Standard HTTP response.\r
270    */\r
271   @PUT\r
272   @Path("/relationships/{type}/{id}")\r
273   @Consumes({MediaType.APPLICATION_JSON})\r
274   @Produces({MediaType.APPLICATION_JSON})\r
275   public Response upsertEdge(String content,\r
276                              @PathParam("type") String type,\r
277                              @PathParam("id") String id,\r
278                              @PathParam("uri") @Encoded String uri,\r
279                              @Context HttpHeaders headers,\r
280                              @Context UriInfo uriInfo,\r
281                              @Context HttpServletRequest req) {\r
282     LoggingUtil.initMdcContext(req, headers);\r
283 \r
284     logger.debug("Incoming request..." + content);\r
285     Response response = null;\r
286 \r
287     if (validateRequest(req, uri, content, Action.PUT, CrudServiceConstants.CRD_AUTH_POLICY_NAME)) {\r
288 \r
289       try {\r
290 \r
291         // Extract the edge payload from the request.\r
292         EdgePayload payload = EdgePayload.fromJson(content);\r
293 \r
294         // Do some basic validation on the payload.\r
295         if (payload.getProperties() == null || payload.getProperties().isJsonNull()) {\r
296           throw new CrudException("Invalid request Payload", Status.BAD_REQUEST);\r
297         }\r
298         if (payload.getId() != null && !payload.getId().equals(id)) {\r
299           throw new CrudException("ID Mismatch", Status.BAD_REQUEST);\r
300         }\r
301 \r
302         // Apply the edge rules to our edge.\r
303         payload = applyEdgeRulesToPayload(payload);\r
304         ImmutablePair<EntityTag, String> result;\r
305         if (headers.getRequestHeaders().getFirst(HTTP_PATCH_METHOD_OVERRIDE) != null &&\r
306             headers.getRequestHeaders().getFirst(HTTP_PATCH_METHOD_OVERRIDE).equalsIgnoreCase("PATCH")) {\r
307           result = graphDataService.patchEdge(EdgeRulesLoader.getLatestSchemaVersion(), id, type, payload);\r
308           response = Response.status(Status.OK).entity(result.getValue()).type(mediaType).tag(result.getKey()).build();\r
309         } else {\r
310           result = graphDataService.updateEdge(EdgeRulesLoader.getLatestSchemaVersion(), id, type, payload);\r
311           response = Response.status(Status.OK).entity(result.getValue()).type(mediaType).tag(result.getKey()).build();\r
312         }\r
313 \r
314       } catch (CrudException ce) {\r
315         response = Response.status(ce.getHttpStatus()).entity(ce.getMessage()).build();\r
316       } catch (Exception e) {\r
317         response = Response.status(Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).build();\r
318       }\r
319 \r
320     } else {\r
321 \r
322       response = Response.status(Status.FORBIDDEN).entity(content)\r
323           .type(MediaType.APPLICATION_JSON).build();\r
324     }\r
325 \r
326     LoggingUtil.logRestRequest(logger, auditLogger, req, response);\r
327     return response;\r
328   }\r
329 \r
330 \r
331   /**\r
332    * Retrieves the properties defined in the edge rules for a relationship between the\r
333    * supplied vertex types.\r
334    *\r
335    * @param sourceVertexType - Type of source vertex for the relationship.\r
336    * @param targetVertexType - Type of target vertex for the relationship.\r
337    *\r
338    * @return - The defined properties for the relationship type.\r
339    *\r
340    * @throws CrudException\r
341    */\r
342   private Map<EdgeProperty, String> getEdgeRuleProperties(String sourceVertexType, String targetVertexType) throws CrudException {\r
343 \r
344     if(logger.isDebugEnabled()) {\r
345       logger.debug("Lookup db edge rules for " + sourceVertexType + " -> " + targetVertexType);\r
346     }\r
347 \r
348     EdgeRules rules = EdgeRules.getInstance();\r
349     EdgeRule rule;\r
350     try {\r
351 \r
352       if(logger.isDebugEnabled()) {\r
353         logger.debug("Lookup by edge type TREE");\r
354       }\r
355 \r
356       // We have no way of knowing in advance whether our relationship is considered to\r
357       // be a tree or cousing relationship, so try looking it up as a tree type first.\r
358       rule = rules.getEdgeRule(EdgeType.TREE, sourceVertexType, targetVertexType);\r
359 \r
360     } catch (AAIException e) {\r
361       try {\r
362 \r
363         if(logger.isDebugEnabled()) {\r
364           logger.debug("Lookup by edge type COUSIN");\r
365         }\r
366 \r
367         // If we are here, then our lookup by 'tree' type failed, so try looking it up\r
368         // as a 'cousin' relationship.\r
369         rule = rules.getEdgeRule(EdgeType.COUSIN, sourceVertexType, targetVertexType);\r
370 \r
371       } catch (AAIException e1) {\r
372 \r
373         // If we're here then we failed to find edge rules for this relationship.  Time to\r
374         // give up...\r
375         throw new CrudException("No edge rules for " + sourceVertexType + " -> " + targetVertexType, Status.NOT_FOUND);\r
376       }\r
377     } catch (Exception e) {\r
378 \r
379       throw new CrudException("General failure getting edge rule properties - " +\r
380                               e.getMessage(), Status.INTERNAL_SERVER_ERROR);\r
381     }\r
382 \r
383     return rule.getEdgeProperties();\r
384   }\r
385 \r
386 \r
387   /**\r
388    * This method takes an inbound edge request payload, looks up the edge rules for the\r
389    * sort of relationship defined in the payload, and automatically applies the defined\r
390    * edge properties to it.\r
391    *\r
392    * @param payload - The original edge request payload\r
393    *\r
394    * @return - An updated edge request payload, with the properties defined in the edge\r
395    *           rules automatically populated.\r
396    *\r
397    * @throws CrudException\r
398    */\r
399   public EdgePayload applyEdgeRulesToPayload(EdgePayload payload) throws CrudException {\r
400 \r
401     // Extract the types for both the source and target vertices.\r
402     String srcType = EdgePayloadUtil.getVertexNodeType(payload.getSource());\r
403     String tgtType = EdgePayloadUtil.getVertexNodeType(payload.getTarget());\r
404 \r
405     // Now, get the default properties for this edge based on the edge rules definition...\r
406     Map<EdgeProperty, String> props = getEdgeRuleProperties(srcType, tgtType);\r
407 \r
408     // ...and merge them with any custom properties provided in the request.\r
409     JsonElement mergedProperties = mergeProperties(payload.getProperties(), props);\r
410     payload.setProperties(mergedProperties);\r
411 \r
412     if(logger.isDebugEnabled()) {\r
413       logger.debug("Edge properties after applying rules for '" + srcType + " -> " + tgtType + "': " + mergedProperties);\r
414     }\r
415 \r
416     return payload;\r
417   }\r
418 \r
419 \r
420   /**\r
421    * Given a set of edge properties extracted from an edge request payload and a set of properties\r
422    * taken from the db edge rules, this method merges them into one set of properties.\r
423    * <p>\r
424    * If the client has attempted to override the defined value for a property in the db edge rules\r
425    * then the request will be rejected as invalid.\r
426    *\r
427    * @param propertiesFromRequest - Set of properties from the edge request.\r
428    * @param propertyDefaults      - Set of properties from the db edge rules.\r
429    *\r
430    * @return - A merged set of properties.\r
431    *\r
432    * @throws CrudException\r
433    */\r
434   @SuppressWarnings("unchecked")\r
435   public JsonElement mergeProperties(JsonElement propertiesFromRequest, Map<EdgeProperty, String> propertyDefaults) throws CrudException {\r
436 \r
437     // Convert the properties from the edge payload into something we can\r
438     // manipulate.\r
439     Set<Map.Entry<String, JsonElement>> properties = new HashSet<Map.Entry<String, JsonElement>>();\r
440     properties.addAll(propertiesFromRequest.getAsJsonObject().entrySet());\r
441 \r
442     Set<String> propertyKeys = new HashSet<String>();\r
443     for(Map.Entry<String, JsonElement> property : properties) {\r
444       propertyKeys.add(property.getKey());\r
445     }\r
446 \r
447     // Now, merge in the properties specified in the Db Edge Rules.\r
448     for(EdgeProperty defProperty : propertyDefaults.keySet()) {\r
449 \r
450       // If the edge rules property was explicitly specified by the\r
451       // client then we will reject the request...\r
452       if(!propertyKeys.contains(defProperty.toString())) {\r
453         properties.add(new AbstractMap.SimpleEntry<String, JsonElement>(defProperty.toString(),\r
454             (new JsonPrimitive(propertyDefaults.get(defProperty)))));\r
455 \r
456       } else {\r
457         throw new CrudException("Property " + defProperty + " defined in db edge rules can not be overriden by the client.",\r
458                                 Status.BAD_REQUEST);\r
459       }\r
460     }\r
461 \r
462     Object[] propArray = properties.toArray();\r
463     StringBuilder sb = new StringBuilder();\r
464     sb.append("{");\r
465     boolean first=true;\r
466     for(int i=0; i<propArray.length; i++) {\r
467 \r
468       Map.Entry<String, JsonElement> entry = (Entry<String, JsonElement>) propArray[i];\r
469       if(!first) {\r
470         sb.append(",");\r
471       }\r
472       sb.append("\"").append(entry.getKey()).append("\"").append(":").append(entry.getValue());\r
473       first=false;\r
474     }\r
475     sb.append("}");\r
476 \r
477     // We're done.  Return the result as a JsonElement.\r
478     return gson.fromJson(sb.toString(), JsonElement.class);\r
479   }\r
480 \r
481 \r
482   /**\r
483    * Invokes authentication validation on an incoming HTTP request.\r
484    *\r
485    * @param req                    - The HTTP request.\r
486    * @param uri                    - HTTP URI\r
487    * @param content                - Payload of the HTTP request.\r
488    * @param action                 - What HTTP action is being performed (GET/PUT/POST/PATCH/DELETE)\r
489    * @param authPolicyFunctionName - Policy function being invoked.\r
490    *\r
491    * @return true  - if the request passes validation,\r
492    *         false - otherwise.\r
493    */\r
494   protected boolean validateRequest(HttpServletRequest req,\r
495                                     String uri,\r
496                                     String content,\r
497                                     Action action,\r
498                                     String authPolicyFunctionName) {\r
499     try {\r
500       String cipherSuite = (String) req.getAttribute("javax.servlet.request.cipher_suite");\r
501       String authUser = null;\r
502       if (cipherSuite != null) {\r
503 \r
504         X509Certificate[] certChain = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");\r
505         X509Certificate clientCert = certChain[0];\r
506         X500Principal subjectDn = clientCert.getSubjectX500Principal();\r
507         authUser = subjectDn.toString();\r
508       }\r
509 \r
510       return this.auth.validateRequest(authUser!=null ? authUser.toLowerCase():"", action.toString() + ":" + authPolicyFunctionName);\r
511 \r
512     } catch (Exception e) {\r
513       logResult(action, uri, e);\r
514       return false;\r
515     }\r
516   }\r
517 \r
518   protected void logResult(Action op, String uri, Exception e) {\r
519 \r
520     logger.error(CrudServiceMsgs.EXCEPTION_DURING_METHOD_CALL,\r
521                  op.toString(),\r
522                  uri, Arrays.toString(e.getStackTrace()));\r
523 \r
524     // Clear the MDC context so that no other transaction inadvertently\r
525     // uses our transaction id.\r
526     MDC.clear();\r
527   }\r
528 }\r