From 99d377c6d55a96d9d3cbea55c48df63c906781d4 Mon Sep 17 00:00:00 2001 From: Antoine Vigneau Date: Wed, 8 Feb 2023 10:39:00 +0100 Subject: [PATCH] SONAR-18396 - Support user soft deletion on Azure through SCIM --- .../platform/web/SecurityServletFilter.java | 2 +- .../org/sonarqube/ws/client/BaseRequest.java | 44 +++++----- .../sonarqube/ws/client/HttpConnector.java | 34 ++++---- .../org/sonarqube/ws/client/PatchRequest.java | 45 ++++++++++ .../org/sonarqube/ws/client/PostRequest.java | 56 ++----------- .../ws/client/RequestWithPayload.java | 83 +++++++++++++++++++ .../org/sonarqube/ws/client/WsRequest.java | 2 +- .../ws/client/users/SearchRequest.java | 10 +++ .../ws/client/users/UsersService.java | 3 +- .../sonarqube/ws/client/ServiceTester.java | 6 +- 10 files changed, 194 insertions(+), 91 deletions(-) create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/PatchRequest.java create mode 100644 sonar-ws/src/main/java/org/sonarqube/ws/client/RequestWithPayload.java diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java index 93fb0aadf57..83522dc7c14 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SecurityServletFilter.java @@ -35,7 +35,7 @@ import javax.servlet.http.HttpServletResponse; */ public class SecurityServletFilter implements Filter { - private static final Set ALLOWED_HTTP_METHODS = Set.of("DELETE", "GET", "HEAD", "POST", "PUT"); + private static final Set ALLOWED_HTTP_METHODS = Set.of("DELETE", "GET", "HEAD", "POST", "PUT", "PATCH"); @Override public void init(FilterConfig filterConfig) { diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java index afd4faf870c..6442a6d187d 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java @@ -41,7 +41,7 @@ import static java.util.Objects.requireNonNull; import static org.sonarqube.ws.WsUtils.checkArgument; import static org.sonarqube.ws.WsUtils.isNullOrEmpty; -abstract class BaseRequest implements WsRequest { +abstract class BaseRequest> implements WsRequest { private final String path; @@ -51,7 +51,7 @@ abstract class BaseRequest implements WsRequest { private final DefaultHeaders headers = new DefaultHeaders(); private OptionalInt timeOutInMs = OptionalInt.empty(); private OptionalInt writeTimeOutInMs = OptionalInt.empty(); - + BaseRequest(String path) { this.path = path; } @@ -71,9 +71,9 @@ abstract class BaseRequest implements WsRequest { return timeOutInMs; } - public SELF setTimeOutInMs(int timeOutInMs) { + public T setTimeOutInMs(int timeOutInMs) { this.timeOutInMs = OptionalInt.of(timeOutInMs); - return (SELF) this; + return (T) this; } @Override @@ -81,57 +81,57 @@ abstract class BaseRequest implements WsRequest { return writeTimeOutInMs; } - public SELF setWriteTimeOutInMs(int writeTimeOutInMs) { + public T setWriteTimeOutInMs(int writeTimeOutInMs) { this.writeTimeOutInMs = OptionalInt.of(writeTimeOutInMs); - return (SELF) this; + return (T) this; } /** * Expected media type of response. Default is {@link MediaTypes#JSON}. */ @SuppressWarnings("unchecked") - public SELF setMediaType(String s) { + public T setMediaType(String s) { requireNonNull(s, "media type of response cannot be null"); this.mediaType = s; - return (SELF) this; + return (T) this; } - public SELF setParam(String key, @Nullable String value) { - return setSingleValueParam(key, value); + public T setParam(String key, @Nullable String value) { + return (T) setSingleValueParam(key, value); } - public SELF setParam(String key, @Nullable Integer value) { + public T setParam(String key, @Nullable Integer value) { return setSingleValueParam(key, value); } - public SELF setParam(String key, @Nullable Long value) { + public T setParam(String key, @Nullable Long value) { return setSingleValueParam(key, value); } - public SELF setParam(String key, @Nullable Float value) { + public T setParam(String key, @Nullable Float value) { return setSingleValueParam(key, value); } - public SELF setParam(String key, @Nullable Boolean value) { + public T setParam(String key, @Nullable Boolean value) { return setSingleValueParam(key, value); } @SuppressWarnings("unchecked") - private SELF setSingleValueParam(String key, @Nullable Object value) { + private T setSingleValueParam(String key, @Nullable Object value) { checkArgument(!isNullOrEmpty(key), "a WS parameter key cannot be null"); if (value == null) { - return (SELF) this; + return (T) this; } parameters.setValue(key, value.toString()); - return (SELF) this; + return (T) this; } @SuppressWarnings("unchecked") - public SELF setParam(String key, @Nullable Collection values) { + public T setParam(String key, @Nullable Collection values) { checkArgument(!isNullOrEmpty(key), "a WS parameter key cannot be null"); if (values == null || values.isEmpty()) { - return (SELF) this; + return (T) this; } parameters.setValues(key, values.stream() @@ -139,7 +139,7 @@ abstract class BaseRequest implements WsRequest { .map(Object::toString) .collect(Collectors.toList())); - return (SELF) this; + return (T) this; } @Override @@ -165,10 +165,10 @@ abstract class BaseRequest implements WsRequest { } @SuppressWarnings("unchecked") - public SELF setHeader(String name, @Nullable String value) { + public T setHeader(String name, @Nullable String value) { requireNonNull(name, "Header name can't be null"); headers.setValue(name, value); - return (SELF) this; + return (T) this; } private static class DefaultParameters implements Parameters { diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java index 7ccde46bb66..92739e636f2 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java @@ -36,6 +36,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import org.sonarqube.ws.client.RequestWithPayload.Part; import static java.lang.String.format; import static java.net.HttpURLConnection.HTTP_MOVED_PERM; @@ -111,8 +112,8 @@ public class HttpConnector implements WsConnector { if (httpRequest instanceof GetRequest) { return get((GetRequest) httpRequest); } - if (httpRequest instanceof PostRequest) { - return post((PostRequest) httpRequest); + if (httpRequest instanceof RequestWithPayload) { + return executeRequest((RequestWithPayload) httpRequest); } throw new IllegalArgumentException(format("Unsupported implementation: %s", httpRequest.getClass())); } @@ -125,28 +126,28 @@ public class HttpConnector implements WsConnector { return new OkHttpResponse(doCall(prepareOkHttpClient(okHttpClient, getRequest), okRequestBuilder.build())); } - private WsResponse post(PostRequest postRequest) { - HttpUrl.Builder urlBuilder = prepareUrlBuilder(postRequest); + private WsResponse executeRequest(RequestWithPayload request) { + HttpUrl.Builder urlBuilder = prepareUrlBuilder(request); RequestBody body; - Map parts = postRequest.getParts(); - if (postRequest.hasBody()) { - body = RequestBody.create(JSON, postRequest.getBody()); + Map parts = request.getParts(); + if (request.hasBody()) { + body = RequestBody.create(JSON, request.getBody()); } else if (parts.isEmpty()) { // parameters are defined in the body (application/x-www-form-urlencoded) FormBody.Builder formBody = new FormBody.Builder(); - postRequest.getParameters().getKeys() - .forEach(key -> postRequest.getParameters().getValues(key) + request.getParameters().getKeys() + .forEach(key -> request.getParameters().getValues(key) .forEach(value -> formBody.add(key, value))); body = formBody.build(); } else { // parameters are defined in the URL (as GET) - completeUrlQueryParameters(postRequest, urlBuilder); + completeUrlQueryParameters(request, urlBuilder); MultipartBody.Builder bodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); parts.entrySet().forEach(param -> { - PostRequest.Part part = param.getValue(); + Part part = param.getValue(); bodyBuilder.addFormDataPart( param.getKey(), part.getFile().getName(), @@ -154,9 +155,10 @@ public class HttpConnector implements WsConnector { }); body = bodyBuilder.build(); } - Request.Builder okRequestBuilder = prepareOkRequestBuilder(postRequest, urlBuilder).post(body); - Response response = doCall(prepareOkHttpClient(noRedirectOkHttpClient, postRequest), okRequestBuilder.build()); - response = checkRedirect(response, postRequest); + Request.Builder okRequestBuilder = prepareOkRequestBuilder(request, urlBuilder); + okRequestBuilder = request.addVerbToBuilder(body).apply(okRequestBuilder); + Response response = doCall(prepareOkHttpClient(noRedirectOkHttpClient, request), okRequestBuilder.build()); + response = checkRedirect(response, request); return new OkHttpResponse(response); } @@ -209,7 +211,7 @@ public class HttpConnector implements WsConnector { } } - private Response checkRedirect(Response response, PostRequest postRequest) { + private Response checkRedirect(Response response, RequestWithPayload postRequest) { switch (response.code()) { case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: @@ -226,7 +228,7 @@ public class HttpConnector implements WsConnector { } } - private Response followPostRedirect(Response response, PostRequest postRequest) { + private Response followPostRedirect(Response response, RequestWithPayload postRequest) { String location = response.header("Location"); if (location == null) { throw new IllegalStateException(format("Missing HTTP header 'Location' in redirect of %s", response.request().url())); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/PatchRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/PatchRequest.java new file mode 100644 index 00000000000..15d818c3280 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/PatchRequest.java @@ -0,0 +1,45 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarqube.ws.client; + +import java.util.function.Function; +import okhttp3.Request; +import okhttp3.RequestBody; + +/** + * @since 10.0 + */ +public class PatchRequest extends RequestWithPayload { + + public PatchRequest(String path) { + super(path); + } + + @Override + Function addVerbToBuilder(RequestBody body) { + return builder -> builder.patch(body); + } + + @Override + public Method getMethod() { + return Method.PATCH; + } + +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/PostRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/PostRequest.java index f3e8d7282ae..0a993e25e1b 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/PostRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/PostRequest.java @@ -19,65 +19,27 @@ */ package org.sonarqube.ws.client; -import java.io.File; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.function.Function; +import okhttp3.Request; +import okhttp3.RequestBody; /** * @since 5.3 */ -public class PostRequest extends BaseRequest { - - private String body; - private final Map parts = new LinkedHashMap<>(); +public class PostRequest extends RequestWithPayload { public PostRequest(String path) { super(path); } @Override - public Method getMethod() { - return Method.POST; - } - - public PostRequest setBody(String body) { - this.body = body; - return this; + Function addVerbToBuilder(RequestBody body) { + return builder -> builder.post(body); } - public String getBody() { - return body; - } - - public boolean hasBody() { - return this.body != null; - } - - public PostRequest setPart(String name, Part part) { - this.parts.put(name, part); - return this; - } - - public Map getParts() { - return parts; - } - - public static class Part { - private final String mediaType; - private final File file; - - public Part(String mediaType, File file) { - this.mediaType = mediaType; - this.file = file; - } - - public String getMediaType() { - return mediaType; - } - - public File getFile() { - return file; - } + @Override + public Method getMethod() { + return Method.POST; } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/RequestWithPayload.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/RequestWithPayload.java new file mode 100644 index 00000000000..af767a5d806 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/RequestWithPayload.java @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarqube.ws.client; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import okhttp3.Request; +import okhttp3.RequestBody; + +/** + * @since 5.3 + */ +public abstract class RequestWithPayload> extends BaseRequest> { + + private String body; + private final Map parts = new LinkedHashMap<>(); + + protected RequestWithPayload(String path) { + super(path); + } + + public T setBody(String body) { + this.body = body; + return (T) this; + } + + public String getBody() { + return body; + } + + public boolean hasBody() { + return this.body != null; + } + + public T setPart(String name, Part part) { + this.parts.put(name, part); + return (T) this; + } + + abstract Function addVerbToBuilder(RequestBody body); + + public Map getParts() { + return parts; + } + + public static class Part { + private final String mediaType; + private final File file; + + public Part(String mediaType, File file) { + this.mediaType = mediaType; + this.file = file; + } + + public String getMediaType() { + return mediaType; + } + + public File getFile() { + return file; + } + } + +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java index ddc4c122961..c27274af5b2 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java @@ -51,6 +51,6 @@ public interface WsRequest { Headers getHeaders(); enum Method { - GET, POST + GET, POST, PATCH } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SearchRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SearchRequest.java index c4a60d6236c..577fe35bbc6 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SearchRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SearchRequest.java @@ -33,6 +33,7 @@ public class SearchRequest { private String p; private String ps; private String q; + private Boolean deactivated; /** * Example value: "42" @@ -68,4 +69,13 @@ public class SearchRequest { public String getQ() { return q; } + + public Boolean getDeactivated() { + return deactivated; + } + + public SearchRequest setDeactivated(Boolean deactivated) { + this.deactivated = deactivated; + return this; + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java index da7a112b7b8..078546cde0e 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java @@ -148,7 +148,8 @@ public class UsersService extends BaseService { new GetRequest(path("search")) .setParam("p", request.getP()) .setParam("ps", request.getPs()) - .setParam("q", request.getQ()), + .setParam("q", request.getQ()) + .setParam("deactivated", request.getDeactivated()), SearchWsResponse.parser()); } diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java index 0ee778dde06..6898c537894 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/ServiceTester.java @@ -147,7 +147,7 @@ public class ServiceTester extends ExternalResource { return new RequestAssert<>(getRequest); } - public RequestAssert assertThat(PostRequest postRequest) { + public RequestAssert assertThat(PostRequest postRequest) { return new RequestAssert<>(postRequest); } @@ -163,7 +163,7 @@ public class ServiceTester extends ExternalResource { public PostRequest getPostRequest() { assertSinglePostCall(); - return postCalls.iterator().next().getRequest(); + return (PostRequest) postCalls.iterator().next().getRequest(); } @CheckForNull @@ -198,7 +198,7 @@ public class ServiceTester extends ExternalResource { } @Immutable - public static final class PostCall extends CallWithParser { + public static final class PostCall extends CallWithParser> { public PostCall(PostRequest postRequest, @Nullable Parser parser) { super(postRequest, parser); -- 2.39.5