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 com.google.api.client.auth.oauth2.Credential;
022import org.codehaus.jackson.map.ObjectMapper;
023
024import javax.servlet.http.HttpServletResponse;
025import javax.ws.rs.Consumes;
026import javax.ws.rs.DELETE;
027import javax.ws.rs.POST;
028import javax.ws.rs.PUT;
029import javax.ws.rs.core.Response;
030import javax.ws.rs.core.Response.Status;
031import javax.ws.rs.core.Response.StatusType;
032
033import org.nuxeo.ecm.automation.server.jaxrs.RestOperationException;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.NuxeoException;
036import org.nuxeo.ecm.core.api.NuxeoPrincipal;
037import org.nuxeo.ecm.directory.Session;
038import org.nuxeo.ecm.directory.api.DirectoryService;
039import org.nuxeo.ecm.platform.oauth2.providers.AbstractOAuth2UserEmailProvider;
040import org.nuxeo.ecm.platform.oauth2.providers.NuxeoOAuth2ServiceProvider;
041import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProvider;
042import org.nuxeo.ecm.platform.oauth2.providers.OAuth2ServiceProviderRegistry;
043import org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token;
044import org.nuxeo.ecm.webengine.model.WebObject;
045import org.nuxeo.ecm.webengine.model.impl.AbstractResource;
046import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl;
047import org.nuxeo.runtime.api.Framework;
048
049import javax.servlet.http.HttpServletRequest;
050import javax.ws.rs.GET;
051import javax.ws.rs.Path;
052import javax.ws.rs.PathParam;
053import javax.ws.rs.core.Context;
054import javax.ws.rs.core.MediaType;
055import java.io.IOException;
056import java.io.Serializable;
057import java.util.Collections;
058import java.util.HashMap;
059import java.util.List;
060import java.util.Map;
061import java.util.Objects;
062import java.util.stream.Collectors;
063
064import static org.nuxeo.ecm.platform.oauth2.tokens.NuxeoOAuth2Token.SCHEMA;
065
066/**
067 * Endpoint to retrieve OAuth2 authentication data
068 * @since 8.4
069 */
070@WebObject(type = "oauth2")
071public class OAuth2Object extends AbstractResource<ResourceTypeImpl> {
072
073    public static final String APPLICATION_JSON_NXENTITY = "application/json+nxentity";
074
075    public static final String TOKEN_DIR = "oauth2Tokens";
076
077    /**
078     * Lists all oauth2 service providers.
079     *
080     * @since 9.2
081     */
082    @GET
083    @Path("provider")
084    public List<NuxeoOAuth2ServiceProvider> getProviders(@Context HttpServletRequest request) throws IOException, RestOperationException {
085        return getProviders();
086    }
087
088    /**
089     * Retrieves oauth2 data for a given provider.
090     */
091    @GET
092    @Path("provider/{providerId}")
093    public Response getProvider(@PathParam("providerId") String providerId,
094                                @Context HttpServletRequest request) throws IOException, RestOperationException {
095        return Response.ok(getProvider(providerId)).build();
096    }
097
098    /**
099     * Creates a new OAuth2 service provider.
100     *
101     * @since 9.2
102     */
103    @POST
104    @Path("provider")
105    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
106    public Response addProvider(@Context HttpServletRequest request, NuxeoOAuth2ServiceProvider provider)
107        throws IOException, RestOperationException {
108        checkPermission();
109        Framework.doPrivileged(() -> {
110            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
111            registry.addProvider(provider.getServiceName(),
112                provider.getDescription(),
113                provider.getTokenServerURL(),
114                provider.getAuthorizationServerURL(),
115                provider.getUserAuthorizationURL(),
116                provider.getClientId(),
117                provider.getClientSecret(),
118                provider.getScopes(),
119                provider.isEnabled());
120        });
121        return Response.ok(getProvider(provider.getServiceName())).build();
122    }
123
124    /**
125     * Updates an OAuth2 service provider.
126     *
127     * @since 9.2
128     */
129    @PUT
130    @Path("provider/{providerId}")
131    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
132    public Response updateProvider(@PathParam("providerId") String providerId,
133                                   @Context HttpServletRequest request, NuxeoOAuth2ServiceProvider provider)
134        throws IOException, RestOperationException {
135        checkPermission();
136        getProvider(providerId);
137        Framework.doPrivileged(() -> {
138            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
139            registry.updateProvider(providerId, provider);
140        });
141        return Response.ok(getProvider(provider.getServiceName())).build();
142    }
143
144    /**
145     * Deletes an OAuth2 service provider.
146     *
147     * @since 9.2
148     */
149    @DELETE
150    @Path("provider/{providerId}")
151    public Response deleteProvider(@PathParam("providerId") String providerId, @Context HttpServletRequest request)
152        throws IOException, RestOperationException {
153        checkPermission();
154        getProvider(providerId);
155        Framework.doPrivileged(() -> {
156            OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
157            registry.deleteProvider(providerId);
158        });
159        return Response.noContent().build();
160    }
161
162    /**
163     * Retrieves a valid access token for a given provider and the current user.
164     * If expired, the token will be refreshed.
165     */
166    @GET
167    @Path("provider/{providerId}/token")
168    public Response getToken(@PathParam("providerId") String providerId,
169                             @Context HttpServletRequest request) throws IOException, RestOperationException {
170
171        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
172
173        String username = request.getUserPrincipal().getName();
174        NuxeoOAuth2Token token = getToken(provider, username);
175        if (token == null) {
176            return Response.status(Status.NOT_FOUND).build();
177        }
178        Credential credential = getCredential(provider, token);
179
180        if (credential == null) {
181            return Response.status(Status.NOT_FOUND).build();
182        }
183        Long expiresInSeconds = credential.getExpiresInSeconds();
184        if (expiresInSeconds != null && expiresInSeconds <= 0) {
185            credential.refreshToken();
186        }
187        Map<String,Object> result = new HashMap<>();
188        result.put("token", credential.getAccessToken());
189        return buildResponse(Status.OK, result);
190    }
191
192    /**
193     * Retrieves all OAuth2 tokens.
194     *
195     * @since 9.2
196     */
197    @GET
198    @Path("token")
199    public List<NuxeoOAuth2Token> getTokens(@Context HttpServletRequest request)
200        throws IOException, RestOperationException {
201        checkPermission();
202        return getTokens();
203    }
204
205    /**
206     * Retrieves an OAuth2 Token.
207     *
208     * @since 9.2
209     */
210    @GET
211    @Path("token/{providerId}/{nxuser}")
212    public Response getToken(@PathParam("providerId") String providerId,
213                             @PathParam("nxuser") String nxuser,
214                             @Context HttpServletRequest request)
215        throws IOException, RestOperationException {
216        checkPermission();
217        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
218        return Response.ok(getToken(provider, nxuser)).build();
219    }
220
221    /**
222     * Updates an OAuth2 Token.
223     *
224     * @since 9.2
225     */
226    @PUT
227    @Path("token/{providerId}/{nxuser}")
228    @Consumes({ APPLICATION_JSON_NXENTITY, "application/json" })
229    public Response updateToken(@PathParam("providerId") String providerId,
230                                @PathParam("nxuser") String nxuser,
231                                @Context HttpServletRequest request, NuxeoOAuth2Token token)
232        throws IOException, RestOperationException {
233        checkPermission();
234        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
235        return Response.ok(updateToken(provider, nxuser, token)).build();
236    }
237
238    /**
239     * Deletes an OAuth2 Token.
240     *
241     * @since 9.2
242     */
243    @DELETE
244    @Path("token/{providerId}/{nxuser}")
245    public Response deleteToken(@PathParam("providerId") String providerId,
246                                @PathParam("nxuser") String nxuser,
247                                @Context HttpServletRequest request) throws IOException, RestOperationException {
248        checkPermission();
249        NuxeoOAuth2ServiceProvider provider = getProvider(providerId);
250        deleteToken(getTokenDoc(provider, nxuser));
251        return Response.noContent().build();
252    }
253
254    protected List<NuxeoOAuth2ServiceProvider> getProviders() {
255        OAuth2ServiceProviderRegistry registry = Framework.getService(OAuth2ServiceProviderRegistry.class);
256        return registry.getProviders().stream()
257            .filter(NuxeoOAuth2ServiceProvider.class::isInstance)
258            .map(provider -> (NuxeoOAuth2ServiceProvider) provider)
259            .collect(Collectors.toList());
260    }
261
262    protected NuxeoOAuth2ServiceProvider getProvider(String providerId) throws RestOperationException {
263        OAuth2ServiceProvider provider = Framework.getService(OAuth2ServiceProviderRegistry.class)
264            .getProvider(providerId);
265        if (provider == null || !(provider instanceof NuxeoOAuth2ServiceProvider)) {
266            RestOperationException err = new RestOperationException("Invalid provider: " + providerId);
267            err.setStatus(HttpServletResponse.SC_NOT_FOUND);
268            throw err;
269        }
270        return (NuxeoOAuth2ServiceProvider) provider;
271    }
272
273    protected List<NuxeoOAuth2Token> getTokens() {
274        return getTokens((String)null);
275    }
276
277    protected List<NuxeoOAuth2Token> getTokens(String nxuser) {
278        return Framework.doPrivileged(() -> {
279            DirectoryService ds = Framework.getService(DirectoryService.class);
280            try (Session session = ds.open(TOKEN_DIR)) {
281                Map<String, Serializable> filter = new HashMap<>();
282                if (nxuser != null) {
283                    filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
284                }
285                List<DocumentModel> docs = session.query(filter, Collections.emptySet(), Collections.emptyMap(),
286                    true, 0, 0);
287                return docs.stream().map(NuxeoOAuth2Token::new).collect(Collectors.toList());
288            }
289        });
290    }
291
292    protected DocumentModel getTokenDoc(NuxeoOAuth2ServiceProvider provider, String nxuser)
293        throws RestOperationException {
294        Map<String, Serializable> filter = new HashMap<>();
295        filter.put("serviceName", provider.getServiceName());
296        filter.put(NuxeoOAuth2Token.KEY_NUXEO_LOGIN, nxuser);
297        List<DocumentModel> tokens = Framework.doPrivileged(() -> {
298            List<DocumentModel> entries = provider.getCredentialDataStore().query(filter);
299            return entries.stream().filter(Objects::nonNull).collect(Collectors.toList());
300        });
301        if (tokens.size() > 1) {
302            throw new NuxeoException("Found multiple " + provider.getId() + " accounts for " + nxuser);
303        } else if (tokens.size() == 0) {
304            throw new RestOperationException("No token found for provider: " + provider.getId(), HttpServletResponse.SC_NOT_FOUND);
305        } else {
306            return tokens.get(0);
307        }
308    }
309
310    protected NuxeoOAuth2Token getToken(NuxeoOAuth2ServiceProvider provider, String nxuser)
311        throws RestOperationException {
312        return new NuxeoOAuth2Token(getTokenDoc(provider, nxuser));
313    }
314
315    protected NuxeoOAuth2Token updateToken(NuxeoOAuth2ServiceProvider provider, String nxuser, NuxeoOAuth2Token token)
316        throws RestOperationException {
317        DocumentModel entry = getTokenDoc(provider, nxuser);
318        entry.setProperty(SCHEMA, "serviceName", token.getServiceName());
319        entry.setProperty(SCHEMA, "nuxeoLogin", token.getNuxeoLogin());
320        entry.setProperty(SCHEMA, "clientId", token.getClientId());
321        entry.setProperty(SCHEMA, "isShared", token.isShared());
322        entry.setProperty(SCHEMA, "sharedWith", token.getSharedWith());
323        entry.setProperty(SCHEMA, "serviceLogin", token.getServiceLogin());
324        entry.setProperty(SCHEMA, "creationDate", token.getCreationDate());
325        Framework.doPrivileged(() -> {
326            DirectoryService ds = Framework.getService(DirectoryService.class);
327            try (Session session = ds.open(TOKEN_DIR)) {
328                session.updateEntry(entry);
329            }
330        });
331        return getToken(provider, nxuser);
332    }
333
334    protected void deleteToken(DocumentModel token) throws RestOperationException {
335        Framework.doPrivileged(() -> {
336            DirectoryService ds = Framework.getService(DirectoryService.class);
337            try (Session session = ds.open(TOKEN_DIR)) {
338                session.deleteEntry(token);
339            }
340        });
341    }
342
343    protected Credential getCredential(NuxeoOAuth2ServiceProvider provider, NuxeoOAuth2Token token) {
344        return provider.loadCredential(
345            (provider instanceof AbstractOAuth2UserEmailProvider) ? token.getServiceLogin() : token.getNuxeoLogin());
346    }
347
348    protected Response buildResponse(StatusType status, Object obj) throws IOException {
349        ObjectMapper mapper = new ObjectMapper();
350        String message = mapper.writeValueAsString(obj);
351
352        return Response.status(status)
353            .header("Content-Length", message.getBytes("UTF-8").length)
354            .type(MediaType.APPLICATION_JSON + "; charset=UTF-8")
355            .entity(message)
356            .build();
357    }
358
359    protected void checkPermission() throws RestOperationException {
360        if (!hasPermission()) {
361            throw new RestOperationException("You do not have permissions to perform this operation.",
362                HttpServletResponse.SC_FORBIDDEN);
363        }
364    }
365
366    protected boolean hasPermission() {
367        return ((NuxeoPrincipal) getContext().getCoreSession().getPrincipal()).isAdministrator();
368    }
369
370}