001/*
002 * (C) Copyright 2016 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Gabriel Barata <gbarata@nuxeo.com>
018 */
019package org.nuxeo.ecm.restapi.server.jaxrs;
020
021import static org.nuxeo.ecm.platform.oauth2.Constants.TOKEN_SERVICE;
022import static org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token.SCHEMA;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Objects;
031import java.util.stream.Collectors;
032
033import javax.servlet.http.HttpServletRequest;
034import javax.ws.rs.Consumes;
035import javax.ws.rs.DELETE;
036import javax.ws.rs.GET;
037import javax.ws.rs.POST;
038import javax.ws.rs.PUT;
039import javax.ws.rs.Path;
040import javax.ws.rs.PathParam;
041import javax.ws.rs.core.Context;
042import javax.ws.rs.core.MediaType;
043import javax.ws.rs.core.Response;
044import javax.ws.rs.core.Response.Status;
045import javax.ws.rs.core.Response.StatusType;
046
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.NuxeoPrincipal;
050import org.nuxeo.ecm.directory.Session;
051import org.nuxeo.ecm.directory.api.DirectoryService;
052import org.nuxeo.ecm.platform.oauth2.clients.OAuth2Client;
053import org.nuxeo.ecm.platform.oauth2.clients.OAuth2ClientService;
054import org.nuxeo.ecm.platform.oauth2.providers.AbstractOAuth2UserEmailProvider;
055import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider;
056import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider;
057import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProviderRegistry;
058import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
059import org.nuxeo.ecm.platform.oauth2.tokens.OAuth2TokenStore;
060import org.nuxeo.ecm.webengine.model.WebObject;
061import org.nuxeo.ecm.webengine.model.exceptions.WebResourceNotFoundException;
062import org.nuxeo.ecm.webengine.model.exceptions.WebSecurityException;
063import org.nuxeo.ecm.webengine.model.impl.AbstractResource;
064import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl;
065import org.nuxeo.runtime.api.Framework;
066
067import com.fasterxml.jackson.databind.ObjectMapper;
068import com.google.api.client.auth.oauth2.Credential;
069
070/**
071 * Endpoint to retrieve OAuth2 authentication data
072 *
073 * @since 8.4
074 */
075@WebObject(type = "oauth2")
076public class OAuth2Object extends AbstractResource<ResourceTypeImpl> {
077
078    public static final String APPLICATION_JSON_NXENTITY = "application/json+nxentity";
079
080    public static final String TOKEN_DIR = "oauth2Tokens";
081
082    /**
083     * Lists all oauth2 service providers.
084     *
085     * @since 9.2
086     */
087    @GET
088    @Path("provider")
089    public List<NuxeoOAuth2ServiceProvider> getProviders(@Context HttpServletRequest request) {
090        return getProviders();
091    }
092
093    /**
094     * Retrieves oauth2 data for a given provider.
095     */
096    @GET
097    @Path("provider/{providerId}")
098    public Response getProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request) {
099        return Response.ok(getProvider(providerId)).build();
100    }
101
102    /**
103     * Creates a new OAuth2 service provider.
104     *
105     * @since 9.2
106     */
107    @POST
108    @Path("provider")
109    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
110    public Response addProvider(@Context HttpServletRequest request, NuxeoOAuth2ServiceProvider provider) {
111        checkPermission(null);
112        Framework.doPrivileged(() -> {
113            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
114            registry.addProvider(provider.getServiceName(), provider.getDescription(), provider.getTokenServerURL(),
115                    provider.getAuthorizationServerURL(), provider.getUserAuthorizationURL(), provider.getClientId(),
116                    provider.getClientSecret(), provider.getScopes(), provider.isEnabled());
117        });
118        return Response.ok(getProvider(provider.getServiceName())).build();
119    }
120
121    /**
122     * Updates an OAuth2 service provider.
123     *
124     * @since 9.2
125     */
126    @PUT
127    @Path("provider/{providerId}")
128    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
129    public Response updateProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request,
130            NuxeoOAuth2ServiceProvider provider) {
131        checkPermission(null);
132        getProvider(providerId);
133        Framework.doPrivileged(() -> {
134            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
135            registry.updateProvider(providerId, provider);
136        });
137        return Response.ok(getProvider(provider.getServiceName())).build();
138    }
139
140    /**
141     * Deletes an OAuth2 service provider.
142     *
143     * @since 9.2
144     */
145    @DELETE
146    @Path("provider/{providerId}")
147    public Response deleteProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request) {
148        checkPermission(null);
149        getProvider(providerId);
150        Framework.doPrivileged(() -> {
151            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
152            registry.deleteProvider(providerId);
153        });
154        return Response.noContent().build();
155    }
156
157    /**
158     * Retrieves a valid access token for a given provider and the current user. If expired, the token will be
159     * refreshed.
160     */
161    @GET
162    @Path("provider/{providerId}/token")
163    public Response getToken(@PathParam("providerId") String providerId, @Context HttpServletRequest request)
164            throws IOException {
165
166        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
167
168        String username = request.getUserPrincipal().getName();
169        NuxeoOAuth2Token token = getToken(provider, username);
170        if (token == null) {
171            return Response.status(Status.NOT_FOUND).build();
172        }
173        Credential credential = getCredential(provider, token);
174
175        if (credential == null) {
176            return Response.status(Status.NOT_FOUND).build();
177        }
178        Long expiresInSeconds = credential.getExpiresInSeconds();
179        if (expiresInSeconds != null && expiresInSeconds <= 0) {
180            credential.refreshToken();
181        }
182        Map<String, Object> result = new HashMap<>();
183        result.put("token", credential.getAccessToken());
184        return buildResponse(Status.OK, result);
185    }
186
187    /**
188     * Retrieves all OAuth2 tokens.
189     *
190     * @since 9.2
191     */
192    @GET
193    @Path("token")
194    public List<NuxeoOAuth2Token> getTokens(@Context HttpServletRequest request) {
195        checkPermission(null);
196        return getTokens();
197    }
198
199    /**
200     * Retrieves an OAuth2 provider token.
201     *
202     * @since 10.2
203     */
204    @GET
205    @Path("token/provider/{providerId}/user/{nxuser}")
206    public Response getProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
207                                     @Context HttpServletRequest request) {
208        checkPermission(nxuser);
209        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
210        return Response.ok(getToken(provider, nxuser)).build();
211    }
212
213    /**
214     * Retrieves an OAuth2 Token.
215     *
216     * @since 9.2
217     *
218     * @deprecated since 10.2 Use {@link #getProviderToken(String, String, HttpServletRequest)} instead.
219     */
220    @Deprecated
221    @GET
222    @Path("token/{providerId}/{nxuser}")
223    public Response getToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
224                             @Context HttpServletRequest request) {
225        return getProviderToken(providerId, nxuser, request);
226    }
227
228    /**
229     * Updates an OAuth2 provider token.
230     *
231     * @since 10.2
232     */
233    @PUT
234    @Path("token/provider/{providerId}/user/{nxuser}")
235    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
236    public Response updateProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
237                                        @Context HttpServletRequest request, NuxeoOAuth2Token token) {
238        checkPermission(nxuser);
239        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
240        return Response.ok(updateToken(provider, nxuser, token)).build();
241    }
242
243    /**
244     * Updates an OAuth2 Token.
245     *
246     * @since 9.2
247     *
248     * @deprecated since 10.2 Use {@link #updateProviderToken(String, String, HttpServletRequest, NuxeoOAuth2Token)}
249     * instead.
250     */
251    @Deprecated
252    @PUT
253    @Path("token/{providerId}/{nxuser}")
254    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
255    public Response updateToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
256                                @Context HttpServletRequest request, NuxeoOAuth2Token token) {
257        return updateProviderToken(providerId, nxuser, request, token);
258    }
259
260    /**
261     * Deletes an OAuth2 provider token.
262     *
263     * @since 10.2
264     */
265    @DELETE
266    @Path("token/provider/{providerId}/user/{nxuser}")
267    public Response deleteProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
268                                        @Context HttpServletRequest request) {
269        checkPermission(nxuser);
270        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
271        deleteToken(getTokenDoc(provider, nxuser));
272        return Response.noContent().build();
273    }
274
275    /**
276     * Deletes an OAuth2 Token.
277     *
278     * @since 9.2
279     *
280     * @deprecated since 10.2 Use {@link #deleteProviderToken(String, String, HttpServletRequest)} instead.
281     */
282    @Deprecated
283    @DELETE
284    @Path("token/{providerId}/{nxuser}")
285    public Response deleteToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
286                                @Context HttpServletRequest request) {
287        return deleteProviderToken(providerId, nxuser, request);
288    }
289
290    /**
291     * Retrieves all oauth2 provider tokens for the current user.
292     *
293     * @since 10.2
294     */
295    @GET
296    @Path("token/provider")
297    public List<NuxeoOAuth2Token> getProviderUserTokens(@Context HttpServletRequest request) {
298        checkNotAnonymousUser();
299        String nxuser = request.getUserPrincipal().getName();
300        return getTokens(nxuser).stream() // filter: make sure no client tokens are retrieved
301                .filter(token -> token.getClientId() == null).collect(Collectors.toList());
302    }
303
304    /**
305     * Retrieves all oauth2 client tokens for the current user.
306     *
307     * @since 10.2
308     */
309    @GET
310    @Path("token/client")
311    public List<NuxeoOAuth2Token> getClientUserTokens(@Context HttpServletRequest request) {
312        checkNotAnonymousUser();
313        String nxuser = request.getUserPrincipal().getName();
314        return getTokens(nxuser).stream() // filter: make sure no provider tokens are retrieved
315                .filter(token -> token.getClientId() != null).collect(Collectors.toList());
316    }
317
318    /**
319     * Retrieves a oauth2 client token.
320     *
321     * @since 10.2
322     */
323    @GET
324    @Path("token/client/{clientId}/user/{nxuser}")
325    public Response getClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
326                              @Context HttpServletRequest request) {
327        checkPermission(nxuser);
328        OAuth2Client client = getClient(clientId);
329        return Response.ok(getToken(client, nxuser)).build();
330    }
331
332    /**
333     * Updates an OAuth2 client token.
334     *
335     * @since 10.2
336     */
337    @PUT
338    @Path("token/client/{clientId}/user/{nxuser}")
339    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
340    public Response updateClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
341                                @Context HttpServletRequest request, NuxeoOAuth2Token token) {
342        checkPermission(nxuser);
343        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
344        return Response.ok(updateToken(client, nxuser, token)).build();
345    }
346
347    /**
348     * Deletes a oauth2 client token.
349     *
350     * @since 10.2
351     */
352    @DELETE
353    @Path("token/client/{clientId}/user/{nxuser}")
354    public Response deleteClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
355                                 @Context HttpServletRequest request) {
356        checkPermission(nxuser);
357        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
358        deleteToken(getTokenDoc(client, nxuser));
359        return Response.noContent().build();
360    }
361
362    /**
363     * Retrieves oauth2 clients.
364     *
365     * @since 10.2
366     */
367    @GET
368    @Path("client")
369    public List<OAuth2Client> getClients(@Context HttpServletRequest request) {
370        return Framework.getService(OAuth2ClientService.class).getClients();
371    }
372
373    /**
374     * Retrieves a oauth2 client.
375     *
376     * @since 10.2
377     */
378    @GET
379    @Path("client/{clientId}")
380    public Response getClient(@PathParam("clientId") String clientId,
381                              @Context HttpServletRequest request) {
382        OAuth2Client client = getClient(clientId);
383        return Response.ok(client).build();
384    }
385
386    protected List<NuxeoOAuth2ServiceProvider> getProviders() {
387        OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
388        return registry.getProviders()
389                       .stream()
390                       .filter(NuxeoOAuth2ServiceProvider.class::isInstance)
391                       .map(provider -> (NuxeoOAuth2ServiceProvider) provider)
392                       .collect(Collectors.toList());
393    }
394
395    protected NuxeoOAuth2ServiceProvider getProvider(String providerId) {
396        OAuth2ServiceProvider provider = Framework.getService(OAuth2ServiceProviderRegistry.class)
397                                                  .getProvider(providerId);
398        if (provider == null || !(provider instanceof NuxeoOAuth2ServiceProvider)) {
399            throw new WebResourceNotFoundException("Invalid provider: " + providerId);
400        }
401        return (NuxeoOAuth2ServiceProvider) provider;
402    }
403
404    protected List<NuxeoOAuth2Token> getTokens() {
405        return getTokens((String) null);
406    }
407
408    protected List<NuxeoOAuth2Token> getTokens(String nxuser) {
409        return Framework.doPrivileged(() -> {
410            DirectoryService ds = Framework.getService(DirectoryService.class);
411            try (Session session = ds.open(TOKEN_DIR)) {
412                Map<String, Serializable> filter = new HashMap<>();
413                if (nxuser != null) {
414                    filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
415                }
416                List<DocumentModel> docs = session.query(filter, Collections.emptySet(), Collections.emptyMap(), true,
417                        0, 0);
418                return docs.stream().map(NuxeoOAuth2Token::new).collect(Collectors.toList());
419            }
420        });
421    }
422
423    protected OAuth2Client getClient(String clientId) {
424        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
425        if (client == null) {
426            throw new WebResourceNotFoundException("Invalid client: " + clientId);
427        }
428        return client;
429    }
430
431    protected DocumentModel getTokenDoc(NuxeoOAuth2ServiceProvider provider, String nxuser) {
432        Map<String, Serializable> filter = new HashMap<>();
433        filter.put("serviceName", provider.getServiceName());
434        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
435        List<DocumentModel> tokens = Framework.doPrivileged(() -> {
436            List<DocumentModel> entries = provider.getCredentialDataStore().query(filter);
437            return entries.stream().filter(Objects::nonNull).collect(Collectors.toList());
438        });
439        if (tokens.size() > 1) {
440            throw new NuxeoException("Found multiple " + provider.getId() + " accounts for " + nxuser);
441        } else if (tokens.isEmpty()) {
442            throw new WebResourceNotFoundException("No token found for provider: " + provider.getServiceName());
443        } else {
444            return tokens.get(0);
445        }
446    }
447
448    protected DocumentModel getTokenDoc(OAuth2Client client, String nxuser) {
449        Map<String, Serializable> filter = new HashMap<>();
450        filter.put("clientId", client.getId());
451        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
452        OAuth2TokenStore tokenStore = new OAuth2TokenStore(TOKEN_SERVICE);
453        List<DocumentModel> tokens = tokenStore.query(filter).stream()
454                .filter(Objects::nonNull).collect(Collectors.toList());
455        if (tokens.size() > 1) {
456            throw new NuxeoException("Found multiple " + client.getId() + " accounts for " + nxuser);
457        } else if (tokens.size() == 0) {
458            throw new WebResourceNotFoundException("No token found for client: " + client.getId());
459        } else {
460            return tokens.get(0);
461        }
462    }
463
464    protected NuxeoOAuth2Token getToken(NuxeoOAuth2ServiceProvider provider, String nxuser) {
465        return new NuxeoOAuth2Token(getTokenDoc(provider, nxuser));
466    }
467
468    protected NuxeoOAuth2Token getToken(OAuth2Client client, String nxuser) {
469        return new NuxeoOAuth2Token(getTokenDoc(client, nxuser));
470    }
471
472    protected NuxeoOAuth2Token updateToken(NuxeoOAuth2ServiceProvider provider, String nxuser, NuxeoOAuth2Token token) {
473        updateTokenDoc(token, getTokenDoc(provider, nxuser));
474        return getToken(provider, nxuser);
475    }
476
477    protected NuxeoOAuth2Token updateToken(OAuth2Client client, String nxuser, NuxeoOAuth2Token token) {
478        updateTokenDoc(token, getTokenDoc(client, nxuser));
479        return getToken(client, nxuser);
480    }
481
482    protected void updateTokenDoc(NuxeoOAuth2Token token, DocumentModel entry) {
483        entry.setProperty(SCHEMA, "serviceName", token.getServiceName());
484        entry.setProperty(SCHEMA, "nuxeoLogin", token.getNuxeoLogin());
485        entry.setProperty(SCHEMA, "clientId", token.getClientId());
486        entry.setProperty(SCHEMA, "isShared", token.isShared());
487        entry.setProperty(SCHEMA, "sharedWith", token.getSharedWith());
488        entry.setProperty(SCHEMA, "serviceLogin", token.getServiceLogin());
489        entry.setProperty(SCHEMA, "creationDate", token.getCreationDate());
490        Framework.doPrivileged(() -> {
491            DirectoryService ds = Framework.getService(DirectoryService.class);
492            try (Session session = ds.open(TOKEN_DIR)) {
493                session.updateEntry(entry);
494            }
495        });
496    }
497
498    protected void deleteToken(DocumentModel token) {
499        Framework.doPrivileged(() -> {
500            DirectoryService ds = Framework.getService(DirectoryService.class);
501            try (Session session = ds.open(TOKEN_DIR)) {
502                session.deleteEntry(token);
503            }
504        });
505    }
506
507    protected Credential getCredential(NuxeoOAuth2ServiceProvider provider, NuxeoOAuth2Token token) {
508        return provider.loadCredential((provider instanceof AbstractOAuth2UserEmailProvider) ? token.getServiceLogin()
509                : token.getNuxeoLogin());
510    }
511
512    protected Response buildResponse(StatusType status, Object obj) throws IOException {
513        ObjectMapper mapper = new ObjectMapper();
514        String message = mapper.writeValueAsString(obj);
515
516        return Response.status(status)
517                       .header("Content-Length", message.getBytes("UTF-8").length)
518                       .type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
519                       .entity(message)
520                       .build();
521    }
522
523    protected void checkPermission(String nxuser) {
524        if (!hasPermission(nxuser)) {
525            throw new WebSecurityException("You do not have permissions to perform this operation.");
526        }
527    }
528
529    protected boolean hasPermission(String nxuser) {
530        NuxeoPrincipal principal = ((NuxeoPrincipal) getContext().getCoreSession().getPrincipal());
531        return principal.isAdministrator() || (nxuser == null ? false : nxuser.equals(principal.getName()));
532    }
533
534    protected void checkNotAnonymousUser() {
535        NuxeoPrincipal principal = ((NuxeoPrincipal) getContext().getCoreSession().getPrincipal());
536        if (principal.isAnonymous()) {
537            throw new WebSecurityException("You do not have permissions to perform this operation.");
538        }
539    }
540
541}