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}