0fe703c3a0be004ad415da4036bad6d4457172c5
[federation.git] / gateway / src / main / java / org / acumos / federation / gateway / FederationController.java
1 /*-
2  * ===============LICENSE_START=======================================================
3  * Acumos
4  * ===================================================================================
5  * Copyright (C) 2017-2019 AT&T Intellectual Property & Tech Mahindra. All rights reserved.
6  * ===================================================================================
7  * This Acumos software file is distributed by AT&T and Tech Mahindra
8  * under the Apache License, Version 2.0 (the "License");
9  * you may not use this file except in compliance with the License.
10  * You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * This file is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ===============LICENSE_END=========================================================
19  */
20 package org.acumos.federation.gateway;
21
22 import java.lang.invoke.MethodHandles;
23 import java.util.concurrent.Callable;
24 import java.util.List;
25
26 import javax.servlet.http.HttpServletRequest;
27 import javax.servlet.http.HttpServletResponse;
28
29 import io.swagger.annotations.ApiOperation;
30
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import org.springframework.beans.factory.annotation.Autowired;
35 import org.springframework.core.io.InputStreamResource;
36 import org.springframework.core.io.Resource;
37 import org.springframework.http.MediaType;
38 import org.springframework.stereotype.Controller;
39 import org.springframework.web.bind.annotation.CrossOrigin;
40 import org.springframework.web.bind.annotation.ExceptionHandler;
41 import org.springframework.web.bind.annotation.GetMapping;
42 import org.springframework.web.bind.annotation.PathVariable;
43 import org.springframework.web.bind.annotation.PostMapping;
44 import org.springframework.web.bind.annotation.RequestMapping;
45 import org.springframework.web.bind.annotation.RequestParam;
46 import org.springframework.web.bind.annotation.ResponseBody;
47 import org.springframework.web.util.UriTemplateHandler;
48 import org.springframework.security.access.annotation.Secured;
49 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
50
51 import org.acumos.cds.domain.MLPPeer;
52 import org.acumos.cds.domain.MLPCatalog;
53 import org.acumos.cds.domain.MLPSolution;
54 import org.acumos.cds.domain.MLPSolutionRevision;
55 import org.acumos.cds.domain.MLPArtifact;
56 import org.acumos.cds.domain.MLPDocument;
57
58 import org.acumos.federation.client.ClientBase;
59 import org.acumos.federation.client.config.ClientConfig;
60 import org.acumos.federation.client.FederationClient;
61 import org.acumos.federation.client.data.Artifact;
62 import org.acumos.federation.client.data.Document;
63 import org.acumos.federation.client.data.JsonResponse;
64 import org.acumos.federation.client.data.SolutionRevision;
65
66 /**
67  * Controller bean for the external (public) API.
68  */
69 @Controller
70 @CrossOrigin
71 @RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
72 public class FederationController {
73         private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
74
75         @Autowired
76         private FederationConfig federation;
77
78         @Autowired
79         private WebSecurityConfigurerAdapter security;
80
81         @Autowired
82         private PeerService peerService;
83
84         @Autowired
85         private CatalogService catalogService;
86
87         @Autowired
88         private ContentService contentService;
89
90         private UriTemplateHandler originBuilder;
91
92         private String makeOrigin(String uri, Object... params) {
93                 if (originBuilder == null) {
94                         originBuilder = ClientBase.buildRestTemplate("https://" + ((Security)security).getSelf().getSubjectName() + ":" + federation.getServer().getPort(), new ClientConfig(), null, null).getUriTemplateHandler();
95                 }
96                 return originBuilder.expand(uri, params).toString();
97         }
98
99         private void markOrigin(MLPSolution sol) {
100                 if (sol.getOrigin() == null) {
101                         sol.setOrigin(makeOrigin(FederationClient.SOLUTION_URI, sol.getSolutionId()));
102                 }
103         }
104
105         private static String makeFilename(String uri, boolean isnexus) {
106                 if (uri == null) {
107                         return null;
108                 }
109                 String[] urix = uri.split("/");
110                 int len = urix.length;
111                 String tag = "";
112                 int off = urix[len - 1].lastIndexOf('.');
113                 if (isnexus && off != -1) {
114                         tag = urix[len - 1].substring(off);
115                         urix[len - 1] = urix[len - 1].substring(0, off);
116                 }
117                 if (isnexus && len >= 3 && urix[len - 1].equals(urix[len - 3] + '-' + urix[len - 2])) {
118                         return urix[len - 3] + tag;
119                 }
120                 uri = urix[len - 1];
121                 off = uri.indexOf(isnexus? '-': ':');
122                 if (off != -1) {
123                         uri = uri.substring(0, off);
124                 }
125                 return uri + tag;
126         }
127
128         private void markOrigin(MLPArtifact art) {
129                 ((Artifact)art).setFilename(makeFilename(art.getUri(), !FederationClient.ATC_DOCKER.equals(art.getArtifactTypeCode())));
130                 if (!Security.isCurrentPeerLocal()) {
131                         if (art.getUri() != null && FederationClient.ATC_DOCKER.equals(art.getArtifactTypeCode())) {
132                                 art.setDescription(art.getUri());
133                         }
134                         art.setUri(makeOrigin(FederationClient.ARTIFACT_URI, art.getArtifactId()));
135                 }
136         }
137
138         private void markOrigin(MLPDocument doc) {
139                 ((Document)doc).setFilename(makeFilename(doc.getUri(), true));
140                 if (!Security.isCurrentPeerLocal()) {
141                         doc.setUri(makeOrigin(FederationClient.DOCUMENT_URI, doc.getDocumentId()));
142                 }
143         }
144
145         @Secured(Security.ROLE_PEER)
146         @ApiOperation(value = "Invoked by Peer Acumos to get status and self information.", response = MLPPeer.class)
147         @GetMapping(FederationClient.PING_URI)
148         @ResponseBody
149         public JsonResponse<MLPPeer> ping() {
150                 log.debug("/ping");
151                 return respond(((Security)security).getSelf());
152         }
153
154         @Secured(Security.ROLE_PARTNER)
155         @ApiOperation(value = "Invoked by Peer Acumos to get a list of peers from local Acumos Instance .", response = MLPPeer.class, responseContainer = "List")
156         @GetMapping(FederationClient.PEERS_URI)
157         @ResponseBody
158         public JsonResponse<List<MLPPeer>> getPeers() {
159                 log.debug("/peers");
160                 return respond(peerService.getPeers());
161         }
162
163         @Secured(Security.ROLE_REGISTER)
164         @ApiOperation(value = "Invoked by another Acumos Instance to request federation.", response = MLPPeer.class)
165         @PostMapping(FederationClient.REGISTER_URI)
166         @ResponseBody
167         public JsonResponse<MLPPeer> register() {
168                 log.debug("/peer/register");
169                 if (!federation.isRegistrationEnabled()) {
170                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "Not Found");
171                 }
172                 peerService.register();
173                 return respond(((Security)security).getSelf());
174         }
175
176         @Secured(Security.ROLE_UNREGISTER)
177         @ApiOperation(value = "Invoked by another Acumos Instance to request federation termination.", response = MLPPeer.class)
178         @PostMapping(FederationClient.UNREGISTER_URI)
179         @ResponseBody
180         public JsonResponse<MLPPeer> unregister() {
181                 log.debug("/peer/unregister");
182                 if (!federation.isRegistrationEnabled()) {
183                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "Not Found");
184                 }
185                 peerService.unregister();
186                 return respond(((Security)security).getSelf());
187         }
188
189         @Secured(Security.ROLE_PEER)
190         @ApiOperation(value = "Invoked by Peer Acumos to get a list of visible Catalogs from the local Acumos Instance .", response = MLPCatalog.class, responseContainer = "List")
191         @GetMapping(FederationClient.CATALOGS_URI)
192         @ResponseBody
193         public JsonResponse<List<MLPCatalog>> getCatalogs() {
194                 log.debug("/catalogs");
195                 return respond(catalogService.getCatalogs());
196         }
197
198         @Secured(Security.ROLE_PEER)
199         @ApiOperation(value = "Invoked by Peer Acumos to get a list of Published Solutions from the Catalog of the local Acumos Instance .", response = MLPSolution.class, responseContainer = "List")
200         @GetMapping(FederationClient.SOLUTIONS_URI)
201         @ResponseBody
202         public JsonResponse<List<MLPSolution>> getSolutions(@RequestParam(value="catalogId", required = true) String catalogId) {
203                 log.debug("/solutions?catalogId={}", catalogId);
204                 if (!catalogService.isCatalogAllowed(catalogId)) {
205                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No catalog with id " + catalogId);
206                 }
207                 List<MLPSolution> ret = catalogService.getSolutions(catalogId);
208                 for (MLPSolution sol: ret) {
209                         markOrigin(sol);
210                 }
211                 return respond(ret);
212         }
213
214         @Secured(Security.ROLE_PEER)
215         @ApiOperation(value = "Invoked by Peer Acumos to get a list detailed solution information from the Catalog of the local Acumos Instance .", response = MLPSolution.class)
216         @GetMapping(FederationClient.SOLUTION_URI)
217         @ResponseBody
218         public JsonResponse<MLPSolution> getSolution(@PathVariable("solutionId") String solutionId) {
219                 log.debug("/solutions/{}", solutionId);
220                 MLPSolution ret = null;
221                 if (!catalogService.isSolutionAllowed(solutionId) || (ret = catalogService.getSolution(solutionId)) == null) {
222                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No solution with id " + solutionId);
223                 }
224                 markOrigin(ret);
225                 return respond(ret);
226         }
227
228         @Secured(Security.ROLE_PEER)
229         @ApiOperation(value = "Invoked by Peer Acumos to get a list of Solution Revision from the Catalog of the local Acumos Instance .", response = MLPSolutionRevision.class, responseContainer = "List")
230         @GetMapping(FederationClient.REVISIONS_URI)
231         @ResponseBody
232         public JsonResponse<List<MLPSolutionRevision>> getRevisions(@PathVariable("solutionId") String solutionId) {
233                 log.debug("/solutions/{}/revisions", solutionId);
234                 List<MLPSolutionRevision> ret = null;
235                 if (!catalogService.isSolutionAllowed(solutionId) || (ret = catalogService.getRevisions(solutionId)).isEmpty()) {
236                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No solution with id " + solutionId);
237                 }
238                 return respond(ret);
239         }
240
241         @Secured(Security.ROLE_PEER)
242         @ApiOperation(value = "Invoked by peer Acumos to get solution revision details from the local Acumos Instance .", response = MLPSolutionRevision.class)
243         @GetMapping(FederationClient.REVISION_URI)
244         @ResponseBody
245         public JsonResponse<MLPSolutionRevision> getRevision(
246             @PathVariable("solutionId") String solutionId,
247             @PathVariable("revisionId") String revisionId,
248             @RequestParam(value = "catalogId", required = false) String catalogId) {
249                 log.debug("/solutions/{}/revisions/{}?catalogId={}", solutionId, revisionId, catalogId);
250                 if (catalogId != null && !catalogService.isCatalogAllowed(catalogId)) {
251                         catalogId = null;
252                 }
253                 SolutionRevision ret = (SolutionRevision)catalogService.getRevision(revisionId, catalogId);
254                 if (ret == null || !catalogService.isSolutionAllowed(ret.getSolutionId())) {
255                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No revision with id " + revisionId);
256                 }
257                 for (MLPArtifact art: ret.getArtifacts()) {
258                         markOrigin(art);
259                 }
260                 for (MLPDocument doc: ret.getDocuments()) {
261                         markOrigin(doc);
262                 }
263                 return respond(ret);
264         }
265
266         @Secured(Security.ROLE_PEER)
267         @ApiOperation(value = "Invoked by Peer Acumos to get a list of solution revision artifacts from the local Acumos Instance .", response = MLPArtifact.class, responseContainer = "List")
268         @GetMapping(FederationClient.ARTIFACTS_URI)
269         @ResponseBody
270         public JsonResponse<List<MLPArtifact>> getArtifacts(
271             @PathVariable("solutionId") String solutionId,
272             @PathVariable("revisionId") String revisionId) {
273                 log.debug("/solutions/{}/revisions/{}/artifacts", solutionId, revisionId);
274                 if (!catalogService.isRevisionAllowed(revisionId)) {
275                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No revision with id " + revisionId);
276                 }
277                 List<MLPArtifact> ret = catalogService.getArtifacts(revisionId);
278                 for (MLPArtifact art: ret) {
279                         markOrigin(art);
280                 }
281                 return respond(ret);
282         }
283
284         @Secured(Security.ROLE_PEER)
285         @ApiOperation(value = "Invoked by Peer Acumos to get a list of solution revision public documents from the local Acumos Instance .", response = MLPArtifact.class, responseContainer = "List")
286         @GetMapping(FederationClient.DOCUMENTS_URI)
287         @ResponseBody
288         public JsonResponse<List<MLPDocument>> getDocuments(
289             @PathVariable("revisionId") String revisionId,
290             @RequestParam(value = "catalogId", required = true) String catalogId) {
291                 log.debug("/revisions/{}/documents?catalogId={}", revisionId, catalogId);
292                 if (!catalogService.isCatalogAllowed(catalogId)) {
293                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No catalog with id " + catalogId);
294                 }
295                 List<MLPDocument> ret = catalogService.getDocuments(revisionId, catalogId);
296                 for (MLPDocument doc: ret) {
297                         markOrigin(doc);
298                 }
299                 return respond(ret);
300         }
301
302         @Secured(Security.ROLE_PEER)
303         @ApiOperation(value = "API to download artifact content", response = Resource.class, code = 200)
304         @GetMapping(value = FederationClient.ARTIFACT_URI, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
305         @ResponseBody
306         public Callable<Resource> getArtifactContent(@PathVariable("artifactId") String artifactId) {
307                 log.debug("/artifacts/{}/content", artifactId);
308                 if (!catalogService.isArtifactAllowed(artifactId)) {
309                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No artifact with id " + artifactId);
310                 }
311                 return () -> new InputStreamResource(contentService.getArtifactContent(catalogService.getArtifact(artifactId)));
312         }
313
314         @Secured(Security.ROLE_PEER)
315         @ApiOperation(value = "API to download document content", response = Resource.class, code = 200)
316         @GetMapping(value = FederationClient.DOCUMENT_URI, produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
317         @ResponseBody
318         public Resource getDocumentContent(@PathVariable("documentId") String documentId) {
319                 log.debug("/documents/{}/content", documentId);
320                 if (!catalogService.isDocumentAllowed(documentId)) {
321                         throw new BadRequestException(HttpServletResponse.SC_NOT_FOUND, "No document with id " + documentId);
322                 }
323                 return new InputStreamResource(contentService.getDocumentContent(catalogService.getDocument(documentId)));
324         }
325
326         private <T> JsonResponse<T> respond(T content) {
327                 JsonResponse<T> ret = new JsonResponse<>();
328                 ret.setContent(content);
329                 return ret;
330         }
331
332         @ExceptionHandler(BadRequestException.class)
333         @ResponseBody
334         public JsonResponse<Void> badRequestError(HttpServletRequest request, HttpServletResponse response, BadRequestException badRequest) {
335                 log.warn("Rejecting invalid request {}: {} {} {}", request.getRequestURI(), badRequest.getMessage(), badRequest.getCode(), badRequest);
336                 JsonResponse<Void> ret = new JsonResponse<>();
337                 ret.setError(badRequest.getMessage());
338                 response.setStatus(badRequest.getCode());
339                 return ret;
340         }
341 }