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}