You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2023/12/07 13:44:50 UTC

(couchdb) 01/01: WIP

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 <rn...@apache.org>
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.