1 /*
2 * Copyright (C) 2017-2019 Centre National d'Etudes Spatiales (CNES).
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 3.0 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
17 * MA 02110-1301 USA
18 */
19 package fr.cnes.doi.application;
20
21 import static fr.cnes.doi.client.ClientMDS.SCHEMA_DATACITE;
22
23 import org.apache.logging.log4j.LogManager;
24 import org.apache.logging.log4j.Logger;
25 import org.restlet.Context;
26 import org.restlet.Request;
27 import org.restlet.Response;
28 import org.restlet.Restlet;
29 import org.restlet.data.Method;
30 import org.restlet.data.Reference;
31 import org.restlet.data.Status;
32 import org.restlet.ext.wadl.ApplicationInfo;
33 import org.restlet.ext.wadl.DocumentationInfo;
34 import org.restlet.ext.wadl.GrammarsInfo;
35 import org.restlet.ext.wadl.IncludeInfo;
36 import org.restlet.routing.Filter;
37 import org.restlet.routing.Router;
38 import org.restlet.routing.Template;
39 import org.restlet.security.ChallengeAuthenticator;
40 import org.restlet.security.MethodAuthorizer;
41
42 import fr.cnes.doi.client.ClientMDS;
43 import fr.cnes.doi.db.AbstractTokenDBHelper;
44 import fr.cnes.doi.exception.DoiRuntimeException;
45 import fr.cnes.doi.resource.mds.DoiResource;
46 import fr.cnes.doi.resource.mds.DoisResource;
47 import fr.cnes.doi.resource.mds.MediaResource;
48 import fr.cnes.doi.resource.mds.MetadataResource;
49 import fr.cnes.doi.resource.mds.MetadatasResource;
50 import fr.cnes.doi.security.TokenSecurity;
51 import fr.cnes.doi.settings.Consts;
52 import fr.cnes.doi.utils.spec.Requirement;
53 import org.apache.logging.log4j.ThreadContext;
54 import org.restlet.data.ClientInfo;
55
56 /**
57 * Provides an application to handle Data Object Identifier within an
58 * organization. A Digital Object Identifier (DOI) is a persistent identifier or
59 * handle used to uniquely identify objects, standardized by the International
60 * Organization. A DOI aims to be "resolvable", usually to some form of access
61 * to the information object to which the DOI refers. This is achieved by
62 * binding the DOI to metadata about the object, such as a URL, where all the
63 * details about the object are accessible. Everytime a URL changes, the
64 * publisher has to update the metadata for the DOI to link to the new URL. It
65 * is the publisher's responsibility to update the DOI database. If he fails to
66 * do so, the DOI resolves to a dead link leaving the DOI useless.
67 * <p>
68 * Two methods of authentication are defined in this application and both is
69 * optional:
70 * <ul>
71 * <li>{@link #createAuthenticator authenticator} by login/password</li>
72 * <li>{@link #createTokenAuthenticator authenticator} by
73 * {@link fr.cnes.doi.resource.admin.TokenResource token}
74 * </ul>
75 * Only the GET can be anonymous whereas POST, PUT, DELETE
76 * {@link #createMethodAuthorizer methods need to be authenticated}.
77 *
78 * <p>
79 * <b>Security</b><br>
80 * <code>
81 * --------------<br>
82 * The authentication is done by the following pipeline:<br>
83 * |Method authorization|-->|Authentication login/pwd|-->|Authentication
84 * token|<br>
85 * Method authorization : Only GET method does not need an authorization
86 * </code>
87 *
88 * <p>
89 * <b>Routing</b><br>
90 * <code>
91 * --------------<br>
92 * <br>
93 * root<br>
94 * |<br>
95 * |__ dois (authorization)<br>
96 * |__ dois/{doiName} (authorization)<br>
97 * |__ metadata (authorization)<br>
98 * |__ metadata/{doiName} (authorization)<br>
99 * |__ media/{doiName} (authorization)<br>
100 * </code>
101 *
102 * @see #createInboundRoot the resources related to this application
103 * @see <a href="http://www.doi.org/hb.html">DOI Handbook</a>
104 * @see <a href="https://mds.datacite.org/static/apidoc">API Documentation</a>
105 * @see DoisResource List and create DOI
106 * @see DoiResource Handle a DOI
107 * @see MetadatasResource Create metadata
108 * @see MetadataResource Handle DOI metadata
109 * @see MediaResource Handle media related to metadata
110 *
111 * @author Jean-Christophe Malapert (jean-Christophe Malapert@cnes.fr)
112 */
113 @Requirement(reqId = Requirement.DOI_SRV_010, reqName = Requirement.DOI_SRV_010_NAME)
114 @Requirement(reqId = Requirement.DOI_SRV_020, reqName = Requirement.DOI_SRV_020_NAME)
115 @Requirement(reqId = Requirement.DOI_SRV_030, reqName = Requirement.DOI_SRV_030_NAME)
116 @Requirement(reqId = Requirement.DOI_SRV_040, reqName = Requirement.DOI_SRV_040_NAME)
117 @Requirement(reqId = Requirement.DOI_SRV_050, reqName = Requirement.DOI_SRV_050_NAME)
118 @Requirement(reqId = Requirement.DOI_SRV_060, reqName = Requirement.DOI_SRV_060_NAME)
119 @Requirement(reqId = Requirement.DOI_SRV_070, reqName = Requirement.DOI_SRV_070_NAME)
120 @Requirement(reqId = Requirement.DOI_SRV_080, reqName = Requirement.DOI_SRV_080_NAME)
121 @Requirement(reqId = Requirement.DOI_SRV_090, reqName = Requirement.DOI_SRV_090_NAME)
122 @Requirement(reqId = Requirement.DOI_MONIT_020, reqName = Requirement.DOI_MONIT_020_NAME)
123 public final class DoiMdsApplication extends AbstractApplication {
124
125 /**
126 * Template Query for DOI : {@value #DOI_TEMPLATE}.
127 */
128 public static final String DOI_TEMPLATE = "doiName";
129
130 /**
131 * URI to handle the collection of DOIs : {@value #DOI_URI}.
132 */
133 public static final String DOI_URI = "/dois";
134
135 /**
136 * URI to handle a DOI : {@value #DOI_NAME_URI}.
137 */
138 public static final String DOI_NAME_URI = "/{" + DOI_TEMPLATE + "}";
139
140 /**
141 * URI to handle metadata : {@value #METADATAS_URI}.
142 */
143 public static final String METADATAS_URI = "/metadata";
144
145 /**
146 * URI to handle media : {@value #MEDIA_URI}.
147 */
148 public static final String MEDIA_URI = "/media";
149
150 /**
151 * Application name : {@value #NAME}
152 */
153 public static final String NAME = "Metadata Store Application";
154
155 /**
156 * Logger.
157 */
158 private static final Logger LOG = LogManager.getLogger(DoiMdsApplication.class.getName());
159
160 /**
161 * Client to query Mds Datacite.
162 */
163 private final ClientMDS client;
164
165 /**
166 * Token DB that contains the set of generated token.
167 */
168 private final AbstractTokenDBHelper tokenDB;
169
170 /**
171 * Creates the Digital Object Identifier server application.
172 *
173 * @param client ClientMDS
174 * @throws DoiRuntimeException When the DataCite schema is not available
175 */
176 public DoiMdsApplication(final ClientMDS client) {
177 super();
178 setName(NAME);
179 setDescription(
180 "Provides an application for handling Data Object Identifier at CNES<br/>"
181 + "This application provides 3 API:" + "<ul>" + "<li>dois : DOI minting</li>"
182 + "<li>metadata : Registration of the associated metadata</li>"
183 + "<li>media : Possbility to obtain metadata in various formats and/or get "
184 + "automatic, direct access to an object rather than via the \"landing page\"</li>"
185 + "</ul>");
186 this.client = client;
187 this.tokenDB = TokenSecurity.getInstance().getTokenDB();
188 }
189
190 /**
191 * Creates a router for the DoiMdsApplication.
192 *
193 * This router routes the resources for the Mds application, which is
194 * protected by two authentication mechanisms (optional mechanisms) and an
195 * authorization by method.
196 *
197 * @see DoiMdsApplication#createRouter the router that contains the Mds
198 * resources
199 * @see DoiMdsApplication#createAuthenticator the authentication mechanism
200 * by login/password
201 * @see DoiMdsApplication#createTokenAuthenticator the authentication
202 * mechanism by token
203 * @see DoiMdsApplication#createMethodAuthorizer the method authorization
204 * mechanism
205 * @return Router
206 */
207 @Override
208 public Restlet createInboundRoot() {
209 LOG.traceEntry();
210
211 final Filter logContext = new Filter() {
212 /**
213 * Get the IP client from proxy (if there is one) and set it in
214 * the header.
215 *
216 * @param request request
217 * @param response response
218 * @return 0
219 */
220 @Override
221 protected int beforeHandle(final Request request, final Response response) {
222 final ClientInfo clientInfo = request.getClientInfo();
223 final String ipAddress = request.getHeaders().getFirstValue(
224 Consts.PROXIFIED_IP, clientInfo.getUpstreamAddress()
225 );
226 ThreadContext.put(Consts.LOG_IP_ADDRESS, ipAddress);
227 return Filter.CONTINUE;
228 }
229 };
230
231 // Defines the strategy of authentication (authentication is not required)
232 // - authentication with login/pwd
233 final ChallengeAuthenticator challAuth = createAuthenticatorLoginBased();
234 challAuth.setOptional(true);
235
236 // - authentication with token
237 final ChallengeAuthenticator challTokenAuth = createTokenAuthenticator();
238 challTokenAuth.setOptional(true);
239
240 // Set specific authorization on method after checking authentication
241 final MethodAuthorizer methodAuth = createMethodAuthorizer();
242
243 // set information available for Log4j
244 logContext.setNext(challAuth);
245
246 // create a pipeline of authentication
247 challAuth.setNext(challTokenAuth);
248 challTokenAuth.setNext(methodAuth);
249
250 // Router
251 methodAuth.setNext(createRouter());
252
253 final Filter filter = new SecurityPostProcessingFilter(getContext(), logContext);
254 return LOG.traceExit(filter);
255 }
256
257 /**
258 * Creates the router. The router routes the following resources:
259 * <ul>
260 * <li>{@link DoiMdsApplication#DOI_URI} to create/update a DOI and its
261 * landing page</li>
262 * <li>{@link DoiMdsApplication#DOI_URI} {@link DoiMdsApplication#DOI_NAME_URI}
263 * to get the URL of the landing page related to a given DOI</li>
264 * <li>{@link DoiMdsApplication#METADATAS_URI} to create/update DOI
265 * metadata</li>
266 * <li>{@link DoiMdsApplication#METADATAS_URI} {@link DoiMdsApplication#DOI_NAME_URI}
267 * to get DOI's metadata or delete a given DOI</li>
268 * <li>{@link DoiMdsApplication#MEDIA_URI} {@link DoiMdsApplication#DOI_NAME_URI}
269 * to handle media related to a DOI
270 * </ul>
271 *
272 * @see DoiResource Handles a DOI and its landing page
273 * @see MetadatasResource Handles DOI metadata
274 * @see MetadataResource Handles DOI metadata
275 * @see MediaResource Handles media related to a DOI
276 * @return the router
277 */
278 private Router createRouter() {
279 LOG.traceEntry();
280
281 final Router router = new Router(getContext());
282 router.attach(DOI_URI, DoisResource.class);
283 router.attach(DOI_URI + DOI_NAME_URI, DoiResource.class)
284 .getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
285 router.attach(METADATAS_URI, MetadatasResource.class);
286 router.attach(METADATAS_URI + DOI_NAME_URI, MetadataResource.class)
287 .getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
288 router.attach(MEDIA_URI + DOI_NAME_URI, MediaResource.class)
289 .getTemplate().setMatchingMode(Template.MODE_STARTS_WITH);
290
291 return LOG.traceExit(router);
292 }
293
294 /**
295 * Creates the method authorizer. GET method can be anonymous. The verbs
296 * (POST, PUT, DELETE) need to be authenticated.
297 *
298 * @return Authorizer based on authorized methods
299 */
300 private MethodAuthorizer createMethodAuthorizer() {
301 LOG.traceEntry();
302
303 final MethodAuthorizer methodAuth = new MethodAuthorizer();
304 methodAuth.getAnonymousMethods().add(Method.GET);
305 methodAuth.getAnonymousMethods().add(Method.OPTIONS);
306 methodAuth.getAuthenticatedMethods().add(Method.GET);
307 methodAuth.getAuthenticatedMethods().add(Method.POST);
308 methodAuth.getAuthenticatedMethods().add(Method.PUT);
309 methodAuth.getAuthenticatedMethods().add(Method.DELETE);
310
311 return LOG.traceExit(methodAuth);
312 }
313
314 /**
315 * Returns the decrypted login for DataCite.
316 *
317 * @return the DataCite's login
318 */
319 private String getLoginMds() {
320 LOG.traceEntry();
321 return LOG.traceExit(this.getConfig().getSecret(Consts.INIST_LOGIN));
322 }
323
324 /**
325 * Returns the decrypted password for DataCite.
326 *
327 * @return the DataCite's pwd
328 */
329 private String getPwdMds() {
330 LOG.traceEntry();
331 return LOG.traceExit(this.getConfig().getSecret(Consts.INIST_PWD));
332 }
333
334 /**
335 * Returns the DOI prefix.
336 *
337 * @return the DOI prefix
338 */
339 public String getDataCentrePrefix() {
340 LOG.traceEntry();
341 return LOG.traceExit(this.getConfig().getString(Consts.INIST_DOI));
342 }
343
344 /**
345 * Returns the client.
346 *
347 * @return the client
348 */
349 public ClientMDS getClient() {
350 LOG.traceEntry();
351 return LOG.traceExit(this.client);
352 }
353
354 /**
355 * Returns the token database.
356 *
357 * @return the token database
358 */
359 @Override
360 public AbstractTokenDBHelper getTokenDB() {
361 LOG.traceEntry();
362 return LOG.traceExit(this.tokenDB);
363 }
364
365 /**
366 * Returns the logger.
367 *
368 * @return the logger
369 */
370 @Override
371 public Logger getLog() {
372 return LOG;
373 }
374
375 /**
376 * Method to describe application in the WADL.
377 *
378 * @param request Request
379 * @param response Response
380 * @return the application description for WADL
381 */
382 @Override
383 public final ApplicationInfo getApplicationInfo(final Request request,
384 final Response response) {
385 final ApplicationInfo result = super.getApplicationInfo(request, response);
386 final DocumentationInfo docInfo = new DocumentationInfo(
387 "DOI server application provides is central service that registers DOI at DataCite"
388 );
389 docInfo.setTitle(this.getName());
390 docInfo.setTextContent(this.getDescription());
391 result.setDocumentation(docInfo);
392 result.getNamespaces().put(SCHEMA_DATACITE, "default");
393 result.getNamespaces().put("http://www.w3.org/2001/XMLSchema", "xsi");
394 final GrammarsInfo grammar = new GrammarsInfo();
395 final IncludeInfo include = new IncludeInfo();
396 include.setTargetRef(new Reference(SCHEMA_DATACITE));
397 grammar.getIncludes().add(include);
398 result.setGrammars(grammar);
399 return result;
400 }
401
402 /**
403 * Post processing for specific authorization. Specific class to handle the
404 * case where the user is authorized by oauth but non authorized by the
405 * service because the user's role is not related to any projects
406 */
407 public static class SecurityPostProcessingFilter extends Filter {
408
409 /**
410 * Constructor
411 *
412 * @param context context
413 * @param next gard
414 */
415 public SecurityPostProcessingFilter(final Context context, final Restlet next) {
416 super(context, next);
417 }
418
419 /**
420 * Fixed problem with Jetty response.
421 *
422 * @param request request
423 * @param response response
424 */
425 @Override
426 protected void afterHandle(final Request request, final Response response) {
427 final Status status = response.getStatus();
428 final String reason = status.getReasonPhrase();
429 if (status.getCode() == Status.CLIENT_ERROR_UNAUTHORIZED.getCode()
430 && API_MDS.SECURITY_USER_NO_ROLE.getShortMessage().equals(reason)) {
431 response.getHeaders().add("WWW-Authenticate",
432 "Basic realm=\"DOI Server access\", charset=\"UTF-8\"");
433 }
434 }
435 }
436
437 /**
438 * API related only to the code in this webservice. Other codes can be
439 * returned by the {@link ClientMDS}
440 */
441 public enum API_MDS {
442 /**
443 * Create metadata. SUCCESS_CREATED is used as status meaning "Operation
444 * successful"
445 */
446 CREATE_METADATA(Status.SUCCESS_CREATED, "Operation successful"),
447 /**
448 * Forbidden to use this role. It happens when a user provides a role to
449 * the server whereas he is unknown in this role. CLIENT_ERROR_FORBIDDEN
450 * is used as status meaning "Forbidden to use this role"
451 */
452 SECURITY_USER_NOT_IN_SELECTED_ROLE(Status.CLIENT_ERROR_FORBIDDEN,
453 "Forbidden to use this role"),
454 /**
455 * Fail to authorize the user. It happens when a client is authentified
456 * but unauthorized to use the resource. CLIENT_ERROR_UNAUTHORIZED is
457 * used as status meaning "Fail to authorize the user"
458 */
459 SECURITY_USER_NO_ROLE(Status.CLIENT_ERROR_UNAUTHORIZED, "Fail to authorize the user"),
460 /**
461 * Fail to know privileges of a user. It happens when an user is
462 * associated to several roles without selecting one.
463 * CLIENT_ERROR_CONFLICT is used as status meaning "Error when an user
464 * is associated to more than one role without setting selectedRole
465 * parameter"
466 */
467 SECURITY_USER_CONFLICT(Status.CLIENT_ERROR_CONFLICT,
468 "Error when an user is associated to more than one role without setting selectedRole "
469 + "parameter"),
470 /**
471 * Fail to create a DOI. It happens when a user try to create a DOI with
472 * the wrong role. Actually, the role is contained in the DOI name. So
473 * if a user is authentified with a right role and try to create a DOI
474 * for another role, an exception is raised. SECURITY_USER_PERMISSION is
475 * used as status meaning "User is not allowed to make this operation"
476 */
477 SECURITY_USER_PERMISSION(Status.CLIENT_ERROR_FORBIDDEN,
478 "User is not allowed to make this operation"),
479 /**
480 * Cannot access to Datacite. It happens when a network problem may
481 * happen with Datacite. SERVER_ERROR_GATEWAY_TIMEOUT is used as status
482 * meaning "Cannot access to Datacite"
483 */
484 NETWORK_PROBLEM(Status.CONNECTOR_ERROR_COMMUNICATION, "Cannot access to Datacite"),
485 /**
486 * Fail to validate user input parameter for creating the DOI. It
487 * happens in the following cases:
488 * <ul>
489 * <li>the DOI or metadata are not provided</li>
490 * <li>the prefix is not allowed</li>
491 * <li>some characters are not allowed in the DOI name</li>
492 * </ul>
493 * CLIENT_ERROR_BAD_REQUEST is used as status meaning "Failed to
494 * validate the user inputs parameters"
495 */
496 METADATA_VALIDATION(Status.CLIENT_ERROR_BAD_REQUEST,
497 "Failed to validate the user inputs parameters"),
498 /**
499 * Fail to create the media related to the DOI. CLIENT_ERROR_BAD_REQUEST
500 * is used as status meaning "DOI not provided or one or more of the
501 * specified mime-types or urls are invalid (e.g. non supported
502 * mime-type, not allowed url domain, etc.)"
503 */
504 MEDIA_VALIDATION(Status.CLIENT_ERROR_BAD_REQUEST,
505 "DOI not provided or one or more of the specified mime-types or urls are invalid "
506 + "(e.g. non supported mime-type, not allowed url domain, etc.)"),
507 /**
508 * Fail to create the landing page related to the DOI.
509 * CLIENT_ERROR_BAD_REQUEST is used as status meaning "Validation error
510 * when defining the DOI and its landing page"
511 */
512 LANGING_PAGE_VALIDATION(Status.CLIENT_ERROR_BAD_REQUEST,
513 "Validation error when defining the DOI and its landing page"),
514 /**
515 * DOI validation error. CLIENT_ERROR_BAD_REQUEST is used as status
516 * meaning "Character or prefix not allowed in the DOI"
517 */
518 DOI_VALIDATION(Status.CLIENT_ERROR_BAD_REQUEST, "Character or prefix not allowed in the "
519 + "DOI"),
520 /**
521 * Internal server error. Fail to communicate with DataCite using the
522 * interface specification meaning "Interface problem between Datacite
523 * and DOI-Server"
524 */
525 DATACITE_PROBLEM(Status.SERVER_ERROR_INTERNAL,
526 "Interface problem between Datacite and DOI-Server");
527
528 /**
529 * Message status.
530 */
531 private final Status status;
532 /**
533 * Short message.
534 */
535 private final String shortMessage;
536
537 /**
538 * Constructor.
539 *
540 * @param status status
541 * @param shortMessage short message related to the status
542 */
543 API_MDS(final Status status, final String shortMessage) {
544 this.status = status;
545 this.shortMessage = shortMessage;
546 }
547
548 /**
549 * Returns the status
550 *
551 * @return the status
552 */
553 public Status getStatus() {
554 return this.status;
555 }
556
557 /**
558 * Returns the short message.
559 *
560 * @return the short message
561 */
562 public String getShortMessage() {
563 return this.shortMessage;
564 }
565
566 }
567 }