/** * Copyright (C) 2015 The Gravitee team (http://gravitee.io) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.gravitee.am.identityprovider.http.user; import io.gravitee.am.identityprovider.api.*; import io.gravitee.am.identityprovider.http.HttpIdentityProviderResponse; import io.gravitee.am.identityprovider.http.configuration.HttpIdentityProviderConfiguration; import io.gravitee.am.identityprovider.http.configuration.HttpResourceConfiguration; import io.gravitee.am.identityprovider.http.configuration.HttpResponseErrorCondition; import io.gravitee.am.identityprovider.http.configuration.HttpUsersResourceConfiguration; import io.gravitee.am.identityprovider.http.user.spring.HttpUserProviderConfiguration; import io.gravitee.am.service.exception.AbstractManagementException; import io.gravitee.am.service.exception.TechnicalManagementException; import io.gravitee.common.http.HttpHeader; import io.gravitee.common.http.HttpHeaders; import io.gravitee.common.http.MediaType; import io.gravitee.el.TemplateEngine; import io.reactivex.Completable; import io.reactivex.Maybe; import io.reactivex.Single; import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.reactivex.core.MultiMap; import io.vertx.reactivex.core.buffer.Buffer; import io.vertx.reactivex.ext.web.client.HttpRequest; import io.vertx.reactivex.ext.web.client.HttpResponse; import io.vertx.reactivex.ext.web.client.WebClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Import; import java.lang.reflect.Constructor; import java.util.*; /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) * @author GraviteeSource Team */ @Import({HttpUserProviderConfiguration.class}) public class HttpUserProvider implements UserProvider { private static final Logger LOGGER = LoggerFactory.getLogger(HttpUserProvider.class); private static final String USER_CONTEXT_KEY = "user"; private static final String USER_API_RESPONSE_CONTEXT_KEY = "usersResponse"; @Autowired @Qualifier("idpHttpUsersWebClient") private WebClient client; @Autowired private HttpIdentityProviderConfiguration configuration; @Override public Maybe<User> findByUsername(String username) { try { // prepare context AuthenticationContext authenticationContext = new SimpleAuthenticationContext(); TemplateEngine templateEngine = authenticationContext.getTemplateEngine(); DefaultUser user = new DefaultUser(username); templateEngine.getTemplateContext().setVariable(USER_CONTEXT_KEY, user); // prepare request final HttpUsersResourceConfiguration usersResourceConfiguration = configuration.getUsersResource(); final HttpResourceConfiguration readResourceConfiguration = usersResourceConfiguration.getPaths().getReadResource(); final String readUserURI = usersResourceConfiguration.getBaseURL() + readResourceConfiguration.getBaseURL(); final HttpMethod readUserHttpMethod = HttpMethod.valueOf(readResourceConfiguration.getHttpMethod().toString()); final List<HttpHeader> readUserHttpHeaders = readResourceConfiguration.getHttpHeaders(); final String readUserBody = readResourceConfiguration.getHttpBody(); final Single<HttpResponse<Buffer>> requestHandler = processRequest(templateEngine, readUserURI, readUserHttpMethod, readUserHttpHeaders, readUserBody); return requestHandler .toMaybe() .map(httpResponse -> { final List<HttpResponseErrorCondition> errorConditions = readResourceConfiguration.getHttpResponseErrorConditions(); Map<String, Object> userAttributes = processResponse(templateEngine, errorConditions, httpResponse); return convert(user.getUsername(), userAttributes); }) .onErrorResumeNext(ex -> { if (ex instanceof AbstractManagementException) { return Maybe.error(ex); } LOGGER.error("An error has occurred while searching user {} from the remote HTTP identity provider", username, ex); return Maybe.error(new TechnicalManagementException("An error has occurred while searching user from the remote HTTP identity provider", ex)); }); } catch (Exception ex) { LOGGER.error("An error has occurred while searching the user {}", username, ex); return Maybe.error(new TechnicalManagementException("An error has occurred while searching the user", ex)); } } @Override public Single<User> create(User user) { try { // prepare context AuthenticationContext authenticationContext = new SimpleAuthenticationContext(); TemplateEngine templateEngine = authenticationContext.getTemplateEngine(); templateEngine.getTemplateContext().setVariable(USER_CONTEXT_KEY, user); // prepare request final HttpUsersResourceConfiguration usersResourceConfiguration = configuration.getUsersResource(); final HttpResourceConfiguration createResourceConfiguration = usersResourceConfiguration.getPaths().getCreateResource(); final String createUserURI = usersResourceConfiguration.getBaseURL() + createResourceConfiguration.getBaseURL(); final HttpMethod createUserHttpMethod = HttpMethod.valueOf(createResourceConfiguration.getHttpMethod().toString()); final List<HttpHeader> createUserHttpHeaders = createResourceConfiguration.getHttpHeaders(); final String createUserBody = createResourceConfiguration.getHttpBody(); final Single<HttpResponse<Buffer>> requestHandler = processRequest(templateEngine, createUserURI, createUserHttpMethod, createUserHttpHeaders, createUserBody); return requestHandler .map(httpResponse -> { final List<HttpResponseErrorCondition> errorConditions = createResourceConfiguration.getHttpResponseErrorConditions(); Map<String, Object> userAttributes = processResponse(templateEngine, errorConditions, httpResponse); return convert(user.getUsername(), userAttributes); }) .onErrorResumeNext(ex -> { if (ex instanceof AbstractManagementException) { return Single.error(ex); } LOGGER.error("An error has occurred while creating user {} from the remote HTTP identity provider", user.getUsername(), ex); return Single.error(new TechnicalManagementException("An error has occurred while creating user from the remote HTTP identity provider", ex)); }); } catch (Exception ex) { LOGGER.error("An error has occurred while creating the user {}", user.getUsername(), ex); return Single.error(new TechnicalManagementException("An error has occurred while creating the user", ex)); } } @Override public Single<User> update(String id, User updateUser) { try { // prepare context AuthenticationContext authenticationContext = new SimpleAuthenticationContext(); TemplateEngine templateEngine = authenticationContext.getTemplateEngine(); ((DefaultUser) updateUser).setId(id); templateEngine.getTemplateContext().setVariable(USER_CONTEXT_KEY, updateUser); // prepare request final HttpUsersResourceConfiguration usersResourceConfiguration = configuration.getUsersResource(); final HttpResourceConfiguration updateResourceConfiguration = usersResourceConfiguration.getPaths().getUpdateResource(); final String updateUserURI = usersResourceConfiguration.getBaseURL() + updateResourceConfiguration.getBaseURL(); final HttpMethod updateUserHttpMethod = HttpMethod.valueOf(updateResourceConfiguration.getHttpMethod().toString()); final List<HttpHeader> updateUserHttpHeaders = updateResourceConfiguration.getHttpHeaders(); final String updateUserBody = updateResourceConfiguration.getHttpBody(); final Single<HttpResponse<Buffer>> requestHandler = processRequest(templateEngine, updateUserURI, updateUserHttpMethod, updateUserHttpHeaders, updateUserBody); return requestHandler .map(httpResponse -> { final List<HttpResponseErrorCondition> errorConditions = updateResourceConfiguration.getHttpResponseErrorConditions(); Map<String, Object> userAttributes = processResponse(templateEngine, errorConditions, httpResponse); return convert(updateUser.getUsername(), userAttributes); }) .onErrorResumeNext(ex -> { if (ex instanceof AbstractManagementException) { return Single.error(ex); } LOGGER.error("An error has occurred while updating user {} from the remote HTTP identity provider", updateUser.getUsername(), ex); return Single.error(new TechnicalManagementException("An error has occurred while updating user from the remote HTTP identity provider", ex)); }); } catch (Exception ex) { LOGGER.error("An error has occurred while updating the user {}", updateUser.getUsername(), ex); return Single.error(new TechnicalManagementException("An error has occurred while updating the user", ex)); } } @Override public Completable delete(String id) { try { // prepare context DefaultUser deleteUser = new DefaultUser(null); deleteUser.setId(id); AuthenticationContext authenticationContext = new SimpleAuthenticationContext(); TemplateEngine templateEngine = authenticationContext.getTemplateEngine(); templateEngine.getTemplateContext().setVariable(USER_CONTEXT_KEY, deleteUser); // prepare request final HttpUsersResourceConfiguration usersResourceConfiguration = configuration.getUsersResource(); final HttpResourceConfiguration deleteResourceConfiguration = usersResourceConfiguration.getPaths().getDeleteResource(); final String deleteUserURI = usersResourceConfiguration.getBaseURL() + deleteResourceConfiguration.getBaseURL(); final HttpMethod deleteUserHttpMethod = HttpMethod.valueOf(deleteResourceConfiguration.getHttpMethod().toString()); final List<HttpHeader> deleteUserHttpHeaders = deleteResourceConfiguration.getHttpHeaders(); final String updateUserBody = deleteResourceConfiguration.getHttpBody(); final Single<HttpResponse<Buffer>> requestHandler = processRequest(templateEngine, deleteUserURI, deleteUserHttpMethod, deleteUserHttpHeaders, updateUserBody); return requestHandler .flatMapCompletable(httpResponse -> { final List<HttpResponseErrorCondition> errorConditions = deleteResourceConfiguration.getHttpResponseErrorConditions(); try { processResponse(templateEngine, errorConditions, httpResponse); return Completable.complete(); } catch (Exception ex) { return Completable.error(ex); } }) .onErrorResumeNext(ex -> { if (ex instanceof AbstractManagementException) { return Completable.error(ex); } LOGGER.error("An error has occurred while deleting user {} from the remote HTTP identity provider", id, ex); return Completable.error(new TechnicalManagementException("An error has occurred while deleting user from the remote HTTP identity provider", ex)); }); } catch (Exception ex) { LOGGER.error("An error has occurred while deleting the user {}", id, ex); return Completable.error(new TechnicalManagementException("An error has occurred while deleting the user", ex)); } } private User convert(String username, Map<String, Object> userAttributes) { final String identifierAttribute = configuration.getUsersResource().getIdentifierAttribute(); DefaultUser user = new DefaultUser(username); // set external id user.setId(userAttributes.get(identifierAttribute).toString()); // remove sensitive value if any userAttributes.remove(identifierAttribute); userAttributes.remove("password"); Map<String, Object> claims = new HashMap<>(); userAttributes.forEach((k, v) -> claims.put(k, v)); user.setAdditionalInformation(claims); return user; } private Single<HttpResponse<Buffer>> processRequest(TemplateEngine templateEngine, String httpURI, HttpMethod httpMethod, List<HttpHeader> httpHeaders, String httpBody) { // prepare request final String evaluatedHttpURI = templateEngine.getValue(httpURI, String.class); final HttpRequest<Buffer> httpRequest = client.requestAbs(httpMethod, evaluatedHttpURI); // set headers if (httpHeaders != null) { httpHeaders.forEach(header -> { String extValue = templateEngine.getValue(header.getValue(), String.class); httpRequest.putHeader(header.getName(), extValue); }); } // set body Single<HttpResponse<Buffer>> responseHandler; if (httpBody != null && !httpBody.isEmpty()) { String bodyRequest = templateEngine.getValue(httpBody, String.class); if (!httpRequest.headers().contains(HttpHeaders.CONTENT_TYPE)) { httpRequest.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bodyRequest.length())); responseHandler = httpRequest.rxSendBuffer(Buffer.buffer(bodyRequest)); } else { String contentTypeHeader = httpRequest.headers().get(HttpHeaders.CONTENT_TYPE); switch (contentTypeHeader) { case(MediaType.APPLICATION_JSON): responseHandler = httpRequest.rxSendJsonObject(new JsonObject(bodyRequest)); break; case(MediaType.APPLICATION_FORM_URLENCODED): Map<String, String> queryParameters = format(bodyRequest); MultiMap multiMap = MultiMap.caseInsensitiveMultiMap(); multiMap.setAll(queryParameters); responseHandler = httpRequest.rxSendForm(multiMap); break; default: httpRequest.putHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(bodyRequest.length())); responseHandler = httpRequest.rxSendBuffer(Buffer.buffer(bodyRequest)); } } } else { responseHandler = httpRequest.rxSend(); } return responseHandler; } private Map<String, Object> processResponse(TemplateEngine templateEngine, List<HttpResponseErrorCondition> errorConditions, HttpResponse<Buffer> httpResponse) throws Exception { String responseBody = httpResponse.bodyAsString(); templateEngine.getTemplateContext().setVariable(USER_API_RESPONSE_CONTEXT_KEY, new HttpIdentityProviderResponse(httpResponse, responseBody)); // process response Exception lastException = null; if (errorConditions != null) { Iterator<HttpResponseErrorCondition> iter = errorConditions.iterator(); while (iter.hasNext() && lastException == null) { HttpResponseErrorCondition errorCondition = iter.next(); if (templateEngine.getValue(errorCondition.getValue(), Boolean.class)) { Class<? extends Exception> clazz = (Class<? extends Exception>) Class.forName(errorCondition.getException()); if (errorCondition.getMessage() != null) { String errorMessage = templateEngine.getValue(errorCondition.getMessage(), String.class); Constructor<?> constructor = clazz.getConstructor(String.class); lastException = clazz.cast(constructor.newInstance(new Object[]{errorMessage})); } else { lastException = clazz.newInstance(); } } } } // if remote API call failed, throw exception if (lastException != null) { throw lastException; } if (responseBody == null) { return Collections.emptyMap(); } return responseBody.startsWith("[") ? new JsonArray(responseBody).getJsonObject(0).getMap() : new JsonObject(responseBody).getMap(); } private static Map<String, String> format(String query) { Map<String, String> queryPairs = new LinkedHashMap<String, String>(); String[] pairs = query.split("&"); for (String pair : pairs) { int idx = pair.indexOf("="); queryPairs.put(pair.substring(0, idx), pair.substring(idx + 1)); } return queryPairs; } }