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