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 TOKEN_DIR = "oauth2Tokens";
079
080    /**
081     * Lists all oauth2 service providers.
082     *
083     * @since 9.2
084     */
085    @GET
086    @Path("provider")
087    public List<NuxeoOAuth2ServiceProvider> getProviders(@Context HttpServletRequest request) {
088        return getProviders();
089    }
090
091    /**
092     * Retrieves oauth2 data for a given provider.
093     */
094    @GET
095    @Path("provider/{providerId}")
096    public Response getProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request) {
097        return Response.ok(getProvider(providerId)).build();
098    }
099
100    /**
101     * Creates a new OAuth2 service provider.
102     *
103     * @since 9.2
104     */
105    @POST
106    @Path("provider")
107    @Consumes(MediaType.APPLICATION_JSON)
108    public Response addProvider(@Context HttpServletRequest request, NuxeoOAuth2ServiceProvider provider) {
109        checkPermission(null);
110        Framework.doPrivileged(() -> {
111            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
112            registry.addProvider(provider.getServiceName(), provider.getDescription(), provider.getTokenServerURL(),
113                    provider.getAuthorizationServerURL(), provider.getUserAuthorizationURL(), provider.getClientId(),
114                    provider.getClientSecret(), provider.getScopes(), provider.isEnabled());
115        });
116        return Response.ok(getProvider(provider.getServiceName())).build();
117    }
118
119    /**
120     * Updates an OAuth2 service provider.
121     *
122     * @since 9.2
123     */
124    @PUT
125    @Path("provider/{providerId}")
126    @Consumes(MediaType.APPLICATION_JSON)
127    public Response updateProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request,
128            NuxeoOAuth2ServiceProvider provider) {
129        checkPermission(null);
130        getProvider(providerId);
131        Framework.doPrivileged(() -> {
132            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
133            registry.updateProvider(providerId, provider);
134        });
135        return Response.ok(getProvider(provider.getServiceName())).build();
136    }
137
138    /**
139     * Deletes an OAuth2 service provider.
140     *
141     * @since 9.2
142     */
143    @DELETE
144    @Path("provider/{providerId}")
145    public Response deleteProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request) {
146        checkPermission(null);
147        getProvider(providerId);
148        Framework.doPrivileged(() -> {
149            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
150            registry.deleteProvider(providerId);
151        });
152        return Response.noContent().build();
153    }
154
155    /**
156     * Retrieves a valid access token for a given provider and the current user. If expired, the token will be
157     * refreshed.
158     */
159    @GET
160    @Path("provider/{providerId}/token")
161    public Response getToken(@PathParam("providerId") String providerId, @Context HttpServletRequest request)
162            throws IOException {
163
164        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
165
166        String username = request.getUserPrincipal().getName();
167        NuxeoOAuth2Token token = getToken(provider, username);
168        if (token == null) {
169            return Response.status(Status.NOT_FOUND).build();
170        }
171        Credential credential = getCredential(provider, token);
172
173        if (credential == null) {
174            return Response.status(Status.NOT_FOUND).build();
175        }
176        Long expiresInSeconds = credential.getExpiresInSeconds();
177        if (expiresInSeconds != null && expiresInSeconds <= 0) {
178            credential.refreshToken();
179        }
180        Map<String, Object> result = new HashMap<>();
181        result.put("token", credential.getAccessToken());
182        return buildResponse(Status.OK, result);
183    }
184
185    /**
186     * Retrieves all OAuth2 tokens.
187     *
188     * @since 9.2
189     */
190    @GET
191    @Path("token")
192    public List<NuxeoOAuth2Token> getTokens(@Context HttpServletRequest request) {
193        checkPermission(null);
194        return getTokens();
195    }
196
197    /**
198     * Retrieves an OAuth2 provider token.
199     *
200     * @since 10.2
201     */
202    @GET
203    @Path("token/provider/{providerId}/user/{nxuser}")
204    public Response getProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
205                                     @Context HttpServletRequest request) {
206        checkPermission(nxuser);
207        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
208        return Response.ok(getToken(provider, nxuser)).build();
209    }
210
211    /**
212     * Retrieves an OAuth2 Token.
213     *
214     * @since 9.2
215     *
216     * @deprecated since 10.2 Use {@link #getProviderToken(String, String, HttpServletRequest)} instead.
217     */
218    @Deprecated
219    @GET
220    @Path("token/{providerId}/{nxuser}")
221    public Response getToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
222                             @Context HttpServletRequest request) {
223        return getProviderToken(providerId, nxuser, request);
224    }
225
226    /**
227     * Updates an OAuth2 provider token.
228     *
229     * @since 10.2
230     */
231    @PUT
232    @Path("token/provider/{providerId}/user/{nxuser}")
233    @Consumes(MediaType.APPLICATION_JSON)
234    public Response updateProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
235                                        @Context HttpServletRequest request, NuxeoOAuth2Token token) {
236        checkPermission(nxuser);
237        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
238        return Response.ok(updateToken(provider, nxuser, token)).build();
239    }
240
241    /**
242     * Updates an OAuth2 Token.
243     *
244     * @since 9.2
245     *
246     * @deprecated since 10.2 Use {@link #updateProviderToken(String, String, HttpServletRequest, NuxeoOAuth2Token)}
247     * instead.
248     */
249    @Deprecated
250    @PUT
251    @Path("token/{providerId}/{nxuser}")
252    @Consumes(MediaType.APPLICATION_JSON)
253    public Response updateToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
254                                @Context HttpServletRequest request, NuxeoOAuth2Token token) {
255        return updateProviderToken(providerId, nxuser, request, token);
256    }
257
258    /**
259     * Deletes an OAuth2 provider token.
260     *
261     * @since 10.2
262     */
263    @DELETE
264    @Path("token/provider/{providerId}/user/{nxuser}")
265    public Response deleteProviderToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
266                                        @Context HttpServletRequest request) {
267        checkPermission(nxuser);
268        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
269        deleteToken(getTokenDoc(provider, nxuser));
270        return Response.noContent().build();
271    }
272
273    /**
274     * Deletes an OAuth2 Token.
275     *
276     * @since 9.2
277     *
278     * @deprecated since 10.2 Use {@link #deleteProviderToken(String, String, HttpServletRequest)} instead.
279     */
280    @Deprecated
281    @DELETE
282    @Path("token/{providerId}/{nxuser}")
283    public Response deleteToken(@PathParam("providerId") String providerId, @PathParam("nxuser") String nxuser,
284                                @Context HttpServletRequest request) {
285        return deleteProviderToken(providerId, nxuser, request);
286    }
287
288    /**
289     * Retrieves all oauth2 provider tokens for the current user.
290     *
291     * @since 10.2
292     */
293    @GET
294    @Path("token/provider")
295    public List<NuxeoOAuth2Token> getProviderUserTokens(@Context HttpServletRequest request) {
296        checkNotAnonymousUser();
297        String nxuser = request.getUserPrincipal().getName();
298        return getTokens(nxuser).stream() // filter: make sure no client tokens are retrieved
299                .filter(token -> token.getClientId() == null).collect(Collectors.toList());
300    }
301
302    /**
303     * Retrieves all oauth2 client tokens for the current user.
304     *
305     * @since 10.2
306     */
307    @GET
308    @Path("token/client")
309    public List<NuxeoOAuth2Token> getClientUserTokens(@Context HttpServletRequest request) {
310        checkNotAnonymousUser();
311        String nxuser = request.getUserPrincipal().getName();
312        return getTokens(nxuser).stream() // filter: make sure no provider tokens are retrieved
313                .filter(token -> token.getClientId() != null).collect(Collectors.toList());
314    }
315
316    /**
317     * Retrieves a oauth2 client token.
318     *
319     * @since 10.2
320     */
321    @GET
322    @Path("token/client/{clientId}/user/{nxuser}")
323    public Response getClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
324                              @Context HttpServletRequest request) {
325        checkPermission(nxuser);
326        OAuth2Client client = getClient(clientId);
327        return Response.ok(getToken(client, nxuser)).build();
328    }
329
330    /**
331     * Updates an OAuth2 client token.
332     *
333     * @since 10.2
334     */
335    @PUT
336    @Path("token/client/{clientId}/user/{nxuser}")
337    @Consumes(MediaType.APPLICATION_JSON)
338    public Response updateClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
339                                @Context HttpServletRequest request, NuxeoOAuth2Token token) {
340        checkPermission(nxuser);
341        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
342        return Response.ok(updateToken(client, nxuser, token)).build();
343    }
344
345    /**
346     * Deletes a oauth2 client token.
347     *
348     * @since 10.2
349     */
350    @DELETE
351    @Path("token/client/{clientId}/user/{nxuser}")
352    public Response deleteClientToken(@PathParam("clientId") String clientId, @PathParam("nxuser") String nxuser,
353                                 @Context HttpServletRequest request) {
354        checkPermission(nxuser);
355        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
356        deleteToken(getTokenDoc(client, nxuser));
357        return Response.noContent().build();
358    }
359
360    /**
361     * Retrieves oauth2 clients.
362     *
363     * @since 10.2
364     */
365    @GET
366    @Path("client")
367    public List<OAuth2Client> getClients(@Context HttpServletRequest request) {
368        return Framework.getService(OAuth2ClientService.class).getClients();
369    }
370
371    /**
372     * Retrieves a oauth2 client.
373     *
374     * @since 10.2
375     */
376    @GET
377    @Path("client/{clientId}")
378    public Response getClient(@PathParam("clientId") String clientId,
379                              @Context HttpServletRequest request) {
380        OAuth2Client client = getClient(clientId);
381        return Response.ok(client).build();
382    }
383
384    protected List<NuxeoOAuth2ServiceProvider> getProviders() {
385        OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
386        return registry.getProviders()
387                       .stream()
388                       .filter(NuxeoOAuth2ServiceProvider.class::isInstance)
389                       .map(provider -> (NuxeoOAuth2ServiceProvider) provider)
390                       .collect(Collectors.toList());
391    }
392
393    protected NuxeoOAuth2ServiceProvider getProvider(String providerId) {
394        OAuth2ServiceProvider provider = Framework.getService(OAuth2ServiceProviderRegistry.class)
395                                                  .getProvider(providerId);
396        if (provider == null || !(provider instanceof NuxeoOAuth2ServiceProvider)) {
397            throw new WebResourceNotFoundException("Invalid provider: " + providerId);
398        }
399        return (NuxeoOAuth2ServiceProvider) provider;
400    }
401
402    protected List<NuxeoOAuth2Token> getTokens() {
403        return getTokens((String) null);
404    }
405
406    protected List<NuxeoOAuth2Token> getTokens(String nxuser) {
407        return Framework.doPrivileged(() -> {
408            DirectoryService ds = Framework.getService(DirectoryService.class);
409            try (Session session = ds.open(TOKEN_DIR)) {
410                Map<String, Serializable> filter = new HashMap<>();
411                if (nxuser != null) {
412                    filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
413                }
414                List<DocumentModel> docs = session.query(filter, Collections.emptySet(), Collections.emptyMap(), true,
415                        0, 0);
416                return docs.stream().map(NuxeoOAuth2Token::new).collect(Collectors.toList());
417            }
418        });
419    }
420
421    protected OAuth2Client getClient(String clientId) {
422        OAuth2Client client = Framework.getService(OAuth2ClientService.class).getClient(clientId);
423        if (client == null) {
424            throw new WebResourceNotFoundException("Invalid client: " + clientId);
425        }
426        return client;
427    }
428
429    protected DocumentModel getTokenDoc(NuxeoOAuth2ServiceProvider provider, String nxuser) {
430        Map<String, Serializable> filter = new HashMap<>();
431        filter.put("serviceName", provider.getServiceName());
432        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
433        List<DocumentModel> tokens = Framework.doPrivileged(() -> {
434            List<DocumentModel> entries = provider.getCredentialDataStore().query(filter);
435            return entries.stream().filter(Objects::nonNull).collect(Collectors.toList());
436        });
437        if (tokens.size() > 1) {
438            throw new NuxeoException("Found multiple " + provider.getId() + " accounts for " + nxuser);
439        } else if (tokens.isEmpty()) {
440            throw new WebResourceNotFoundException("No token found for provider: " + provider.getServiceName());
441        } else {
442            return tokens.get(0);
443        }
444    }
445
446    protected DocumentModel getTokenDoc(OAuth2Client client, String nxuser) {
447        Map<String, Serializable> filter = new HashMap<>();
448        filter.put("clientId", client.getId());
449        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
450        OAuth2TokenStore tokenStore = new OAuth2TokenStore(TOKEN_SERVICE);
451        List<DocumentModel> tokens = tokenStore.query(filter).stream()
452                .filter(Objects::nonNull).collect(Collectors.toList());
453        if (tokens.size() > 1) {
454            throw new NuxeoException("Found multiple " + client.getId() + " accounts for " + nxuser);
455        } else if (tokens.size() == 0) {
456            throw new WebResourceNotFoundException("No token found for client: " + client.getId());
457        } else {
458            return tokens.get(0);
459        }
460    }
461
462    protected NuxeoOAuth2Token getToken(NuxeoOAuth2ServiceProvider provider, String nxuser) {
463        return new NuxeoOAuth2Token(getTokenDoc(provider, nxuser));
464    }
465
466    protected NuxeoOAuth2Token getToken(OAuth2Client client, String nxuser) {
467        return new NuxeoOAuth2Token(getTokenDoc(client, nxuser));
468    }
469
470    protected NuxeoOAuth2Token updateToken(NuxeoOAuth2ServiceProvider provider, String nxuser, NuxeoOAuth2Token token) {
471        updateTokenDoc(token, getTokenDoc(provider, nxuser));
472        return getToken(provider, nxuser);
473    }
474
475    protected NuxeoOAuth2Token updateToken(OAuth2Client client, String nxuser, NuxeoOAuth2Token token) {
476        updateTokenDoc(token, getTokenDoc(client, nxuser));
477        return getToken(client, nxuser);
478    }
479
480    protected void updateTokenDoc(NuxeoOAuth2Token token, DocumentModel entry) {
481        entry.setProperty(SCHEMA, "serviceName", token.getServiceName());
482        entry.setProperty(SCHEMA, "nuxeoLogin", token.getNuxeoLogin());
483        entry.setProperty(SCHEMA, "clientId", token.getClientId());
484        entry.setProperty(SCHEMA, "isShared", token.isShared());
485        entry.setProperty(SCHEMA, "sharedWith", token.getSharedWith());
486        entry.setProperty(SCHEMA, "serviceLogin", token.getServiceLogin());
487        entry.setProperty(SCHEMA, "creationDate", token.getCreationDate());
488        Framework.doPrivileged(() -> {
489            DirectoryService ds = Framework.getService(DirectoryService.class);
490            try (Session session = ds.open(TOKEN_DIR)) {
491                session.updateEntry(entry);
492            }
493        });
494    }
495
496    protected void deleteToken(DocumentModel token) {
497        Framework.doPrivileged(() -> {
498            DirectoryService ds = Framework.getService(DirectoryService.class);
499            try (Session session = ds.open(TOKEN_DIR)) {
500                session.deleteEntry(token);
501            }
502        });
503    }
504
505    protected Credential getCredential(NuxeoOAuth2ServiceProvider provider, NuxeoOAuth2Token token) {
506        return provider.loadCredential((provider instanceof AbstractOAuth2UserEmailProvider) ? token.getServiceLogin()
507                : token.getNuxeoLogin());
508    }
509
510    protected Response buildResponse(StatusType status, Object obj) throws IOException {
511        ObjectMapper mapper = new ObjectMapper();
512        String message = mapper.writeValueAsString(obj);
513
514        return Response.status(status)
515                       .header("Content-Length", message.getBytes("UTF-8").length)
516                       .type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
517                       .entity(message)
518                       .build();
519    }
520
521    protected void checkPermission(String nxuser) {
522        if (!hasPermission(nxuser)) {
523            throw new WebSecurityException("You do not have permissions to perform this operation.");
524        }
525    }
526
527    protected boolean hasPermission(String nxuser) {
528        NuxeoPrincipal principal = getContext().getCoreSession().getPrincipal();
529        return principal.isAdministrator() || (nxuser == null ? false : nxuser.equals(principal.getName()));
530    }
531
532    protected void checkNotAnonymousUser() {
533        NuxeoPrincipal principal = getContext().getCoreSession().getPrincipal();
534        if (principal.isAnonymous()) {
535            throw new WebSecurityException("You do not have permissions to perform this operation.");
536        }
537    }
538
539}