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