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