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