This is an automated email from the ASF dual-hosted git repository. rnewson pushed a commit to branch nouveau-auth in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 3ee019a8a43166a4e51829f2367985f49fca8be0 Author: Robert Newson <[email protected]> AuthorDate: Thu Dec 7 13:44:32 2023 +0000 WIP --- .../apache/couchdb/nouveau/NouveauApplication.java | 8 ++++ .../apache/couchdb/nouveau/auth/CouchDBUser.java | 29 +++++++++++ .../couchdb/nouveau/core/UserAgentFilter.java | 56 ++++++++++++++++++++++ src/nouveau/src/nouveau_api.erl | 17 +++---- 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java index 89b2b8596..5f2af45ee 100644 --- a/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/NouveauApplication.java @@ -17,9 +17,12 @@ import com.github.benmanes.caffeine.cache.Scheduler; import io.dropwizard.core.Application; import io.dropwizard.core.setup.Environment; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; +import jakarta.servlet.DispatcherType; +import java.util.EnumSet; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; import org.apache.couchdb.nouveau.core.IndexManager; +import org.apache.couchdb.nouveau.core.UserAgentFilter; import org.apache.couchdb.nouveau.health.AnalyzeHealthCheck; import org.apache.couchdb.nouveau.health.IndexHealthCheck; import org.apache.couchdb.nouveau.lucene9.Lucene9Module; @@ -41,6 +44,11 @@ public class NouveauApplication extends Application<NouveauApplicationConfigurat @Override public void run(NouveauApplicationConfiguration configuration, Environment environment) throws Exception { + // require User-Agent: CouchDB on all requests. + environment + .servlets() + .addFilter("UserAgentFilter", new UserAgentFilter("CouchDB")) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); // configure index manager final IndexManager indexManager = new IndexManager(); diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/auth/CouchDBUser.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/auth/CouchDBUser.java new file mode 100644 index 000000000..7fdc88b2f --- /dev/null +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/auth/CouchDBUser.java @@ -0,0 +1,29 @@ +// 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 org.apache.couchdb.nouveau.auth; + +import java.security.Principal; + +public final class CouchDBUser implements Principal { + + private final String name; + + public CouchDBUser(final String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } +} diff --git a/nouveau/src/main/java/org/apache/couchdb/nouveau/core/UserAgentFilter.java b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/UserAgentFilter.java new file mode 100644 index 000000000..ee534dfc1 --- /dev/null +++ b/nouveau/src/main/java/org/apache/couchdb/nouveau/core/UserAgentFilter.java @@ -0,0 +1,56 @@ +// +// 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 org.apache.couchdb.nouveau.core; + +import com.google.common.net.HttpHeaders; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; +import org.eclipse.jetty.http.HttpStatus; + +public final class UserAgentFilter implements Filter { + + private final String requiredUserAgent; + + public UserAgentFilter(final String requiredUserAgent) { + this.requiredUserAgent = Objects.requireNonNull(requiredUserAgent); + } + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + final String userAgent = ((HttpServletRequest) request).getHeader(HttpHeaders.USER_AGENT); + + if (!requiredUserAgent.equals(userAgent)) { + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpStatus.FORBIDDEN_403); + httpResponse.setContentType("application/json"); + httpResponse + .getWriter() + .print(String.format( + "{\"error\": \"forbidden\", \"reason\": \"User-Agent must be %s\"}\n", + requiredUserAgent)); + } else { + chain.doFilter(request, response); + } + } + } +} diff --git a/src/nouveau/src/nouveau_api.erl b/src/nouveau/src/nouveau_api.erl index 57de204f2..a2abf272e 100644 --- a/src/nouveau/src/nouveau_api.erl +++ b/src/nouveau/src/nouveau_api.erl @@ -34,6 +34,7 @@ ]). -define(JSON_CONTENT_TYPE, {"Content-Type", "application/json"}). +-define(USER_AGENT, {"User-Agent", "CouchDB"}). analyze(Text, Analyzer) when is_binary(Text), is_binary(Analyzer) @@ -305,24 +306,24 @@ errors(Body) -> Json = jiffy:decode(Body, [return_maps]), maps:get(<<"errors">>, Json). -send_if_enabled(Url, Header, Method) -> - send_if_enabled(Url, Header, Method, []). +send_if_enabled(Url, Headers, Method) -> + send_if_enabled(Url, Headers, Method, []). -send_if_enabled(Url, Header, Method, Body) -> - send_if_enabled(Url, Header, Method, Body, []). +send_if_enabled(Url, Headers, Method, Body) -> + send_if_enabled(Url, Headers, Method, Body, []). -send_if_enabled(Url, Header, Method, Body, Options) -> +send_if_enabled(Url, Headers, Method, Body, Options) -> case nouveau:enabled() of true -> - ibrowse:send_req(Url, Header, Method, Body, Options); + ibrowse:send_req(Url, [?USER_AGENT | Headers], Method, Body, Options); false -> {error, nouveau_not_enabled} end. -send_direct_if_enabled(ConnPid, Url, Header, Method, Body, Options) -> +send_direct_if_enabled(ConnPid, Url, Headers, Method, Body, Options) -> case nouveau:enabled() of true -> - ibrowse:send_req_direct(ConnPid, Url, Header, Method, Body, Options); + ibrowse:send_req_direct(ConnPid, Url, [?USER_AGENT | Headers], Method, Body, Options); false -> {error, nouveau_not_enabled} end.
