From 55b613b67727563b537540b324672beb02797f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandeep=20Mor=C3=A9?= Date: Mon, 30 Oct 2023 07:02:58 -0400 Subject: [PATCH] KNOX-2974 - Add a new endpoint 'extauthz' similar to pre that accepts HTTP verbs other than GET and if confgiured ignores additional context path params --- .../service/auth/AbstractAuthResource.java | 120 ++++++++++ .../gateway/service/auth/AuthMessages.java | 3 + .../service/auth/ExtAuthzResource.java | 143 ++++++++++++ .../gateway/service/auth/PreAuthResource.java | 95 +------- .../service/auth/ExtAuthzResourceTest.java | 214 ++++++++++++++++++ 5 files changed, 492 insertions(+), 83 deletions(-) create mode 100644 gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java create mode 100644 gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/ExtAuthzResource.java create mode 100644 gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java diff --git a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java new file mode 100644 index 0000000000..61c9aed766 --- /dev/null +++ b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.knox.gateway.service.auth; + +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.security.SubjectUtils; + +import javax.security.auth.Subject; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static javax.ws.rs.core.Response.ok; +import static javax.ws.rs.core.Response.status; + +public abstract class AbstractAuthResource { + public static final String AUTH_ACTOR_ID_HEADER_NAME = "preauth.auth.header.actor.id.name"; + public static final String AUTH_ACTOR_GROUPS_HEADER_PREFIX = "preauth.auth.header.actor.groups.prefix"; + public static final String GROUP_FILTER_PATTERN = "preauth.group.filter.pattern"; + + static final AuthMessages LOG = MessagesFactory.get(AuthMessages.class); + + static final String DEFAULT_AUTH_ACTOR_ID_HEADER_NAME = "X-Knox-Actor-ID"; + static final String DEFAULT_AUTH_ACTOR_GROUPS_HEADER_PREFIX = "X-Knox-Actor-Groups"; + static final Pattern DEFAULT_GROUP_FILTER_PATTERN = Pattern.compile(".*"); + + protected static final int MAX_HEADER_LENGTH = 1000; + protected static final String ACTOR_GROUPS_HEADER_FORMAT = "%s-%d"; + + protected String authHeaderActorIDName; + protected String authHeaderActorGroupsPrefix; + protected Pattern groupFilterPattern; + + protected void initialize() { + authHeaderActorIDName = getInitParameter(AUTH_ACTOR_ID_HEADER_NAME, DEFAULT_AUTH_ACTOR_ID_HEADER_NAME); + authHeaderActorGroupsPrefix = getInitParameter(AUTH_ACTOR_GROUPS_HEADER_PREFIX, DEFAULT_AUTH_ACTOR_GROUPS_HEADER_PREFIX); + final String groupFilterPatternString = getInitParameter(GROUP_FILTER_PATTERN, null); + groupFilterPattern = groupFilterPatternString == null ? DEFAULT_GROUP_FILTER_PATTERN : Pattern.compile(groupFilterPatternString); + } + + /* abstract method to get the response instance */ + abstract HttpServletResponse getResponse(); + + /* Abstract method that gets context instance */ + abstract ServletContext getContext(); + + String getInitParameter(String paramName, String defaultValue) { + final String initParam = getContext().getInitParameter(paramName); + return initParam == null ? defaultValue : initParam; + } + + public Response doGetImpl() { + final Subject subject = SubjectUtils.getCurrentSubject(); + + final String primaryPrincipalName = subject == null ? null : SubjectUtils.getPrimaryPrincipalName(subject); + if (primaryPrincipalName == null) { + LOG.noPrincipalFound(); + return status(HttpServletResponse.SC_UNAUTHORIZED).build(); + } + getResponse().setHeader(authHeaderActorIDName, primaryPrincipalName); + + // Populate actor groups headers + final Set matchingGroupNames = subject == null ? Collections.emptySet() + : SubjectUtils.getGroupPrincipals(subject).stream().filter(group -> groupFilterPattern.matcher(group.getName()).matches()).map(group -> group.getName()) + .collect(Collectors.toSet()); + if (!matchingGroupNames.isEmpty()) { + final List groupStrings = getGroupStrings(matchingGroupNames); + for (int i = 0; i < groupStrings.size(); i++) { + getResponse().addHeader(String.format(Locale.ROOT, ACTOR_GROUPS_HEADER_FORMAT, authHeaderActorGroupsPrefix, i + 1), groupStrings.get(i)); + } + } + return ok().build(); + } + + private List getGroupStrings(Collection groupNames) { + if (groupNames.isEmpty()) { + return Collections.emptyList(); + } + List groupStrings = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + for (String groupName : groupNames) { + if (sb.length() + groupName.length() > MAX_HEADER_LENGTH) { + groupStrings.add(sb.toString()); + sb = new StringBuilder(); + } + if (sb.length() > 0) { + sb.append(','); + } + sb.append(groupName); + } + if (sb.length() > 0) { + groupStrings.add(sb.toString()); + } + return groupStrings; + } + +} diff --git a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java index e744713f95..662833f26c 100644 --- a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java +++ b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java @@ -27,4 +27,7 @@ public interface AuthMessages { @Message(level = MessageLevel.ERROR, text = "There was a problem extracting authenticated principal from request.") void noPrincipalFound(); + @Message(level = MessageLevel.INFO, text = "Serving request for path: {0}") + void pathValue(String path); + } diff --git a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/ExtAuthzResource.java b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/ExtAuthzResource.java new file mode 100644 index 0000000000..883e0f51fa --- /dev/null +++ b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/ExtAuthzResource.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.knox.gateway.service.auth; + +import javax.annotation.PostConstruct; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +@Path(ExtAuthzResource.RESOURCE_PATH) +public class ExtAuthzResource extends AbstractAuthResource { + static final String RESOURCE_PATH = "auth/api/v1/extauthz"; + static final String IGNORE_ADDITIONAL_PATH = "ignore.additional.path"; + static final String DEFAULT_IGNORE_ADDITIONAL_PATH = "false"; + + private boolean ignoreAdditionalPath; + + @Context + HttpServletResponse response; + + @Context + ServletContext context; + + @PostConstruct + public void init() { + initialize(); + ignoreAdditionalPath = Boolean.parseBoolean(getInitParameter(IGNORE_ADDITIONAL_PATH, DEFAULT_IGNORE_ADDITIONAL_PATH)); + } + + @Override + HttpServletResponse getResponse() { + return response; + } + + @Override + ServletContext getContext() { + return context; + } + + /* + Enable all the http verbs, + wish there was a way to specify multiple paths on the same method + */ + @GET + public Response doGet() { + return doGetImpl(); + } + + @POST + public Response doPostWithRootPath() { + return doGetImpl(); + } + + @PUT + public Response doPutWithRootPath() { + return doGetImpl(); + } + + @DELETE + public Response doDeleteWithRootPath() { + return doGetImpl(); + } + + @HEAD + public Response doHeadWithPath() { + return doGetImpl(); + } + + @OPTIONS + public Response doOptionsWithPath() { + return doGetImpl(); + } + + /* + * This method will handle additional paths. + * Currently, if there are any additional paths they are ignored. + */ + @Path("{subResources:.*}") + @GET + public Response doGetWithPath(@Context UriInfo ui) { + if(ignoreAdditionalPath) { + LOG.pathValue(ui.getPath()); + return doGet(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @Path("{subResources:.*}") + @POST + public Response doPostWithPath(@Context UriInfo ui) { + return doGetWithPath(ui); + } + + @Path("{subResources: .*}") + @PUT + public Response doPutWithPath(@Context UriInfo ui) { + return doGetWithPath(ui); + } + + @Path("{subResources: .*}") + @DELETE + public Response doDeleteWithPath(@Context UriInfo ui) { + return doGetWithPath(ui); + } + + @Path("{subResources: .*}") + @HEAD + public Response doHeadWithPath(@Context UriInfo ui) { + return doGetWithPath(ui); + } + + @Path("{subResources: .*}") + @OPTIONS + public Response doOptionsWithPath(@Context UriInfo ui) { + return doGetWithPath(ui); + } + +} \ No newline at end of file diff --git a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/PreAuthResource.java b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/PreAuthResource.java index 9aaab2a168..d44dd8e734 100644 --- a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/PreAuthResource.java +++ b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/PreAuthResource.java @@ -17,20 +17,7 @@ */ package org.apache.knox.gateway.service.auth; -import static javax.ws.rs.core.Response.ok; -import static javax.ws.rs.core.Response.status; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - import javax.annotation.PostConstruct; -import javax.security.auth.Subject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.GET; @@ -38,91 +25,33 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import org.apache.knox.gateway.i18n.messages.MessagesFactory; -import org.apache.knox.gateway.security.SubjectUtils; - @Path(PreAuthResource.RESOURCE_PATH) -public class PreAuthResource { - static final String RESOURCE_PATH = "auth/api/v1/pre"; - private static final AuthMessages LOG = MessagesFactory.get(AuthMessages.class); - static final String AUTH_ACTOR_ID_HEADER_NAME = "preauth.auth.header.actor.id.name"; - static final String AUTH_ACTOR_GROUPS_HEADER_PREFIX = "preauth.auth.header.actor.groups.prefix"; - static final String GROUP_FILTER_PATTERN = "preauth.group.filter.pattern"; +public class PreAuthResource extends AbstractAuthResource { - static final String DEFAULT_AUTH_ACTOR_ID_HEADER_NAME = "X-Knox-Actor-ID"; - static final String DEFAULT_AUTH_ACTOR_GROUPS_HEADER_PREFIX = "X-Knox-Actor-Groups"; - private static final Pattern DEFAULT_GROUP_FILTER_PATTERN = Pattern.compile(".*"); - - private static final int MAX_HEADER_LENGTH = 1000; - private static final String ACTOR_GROUPS_HEADER_FORMAT = "%s-%d"; + static final String RESOURCE_PATH = "auth/api/v1/pre"; @Context HttpServletResponse response; @Context ServletContext context; - - private String authHeaderActorIDName; - private String authHeaderActorGroupsPrefix; - private Pattern groupFilterPattern; - @PostConstruct public void init() { - authHeaderActorIDName = getInitParameter(AUTH_ACTOR_ID_HEADER_NAME, DEFAULT_AUTH_ACTOR_ID_HEADER_NAME); - authHeaderActorGroupsPrefix = getInitParameter(AUTH_ACTOR_GROUPS_HEADER_PREFIX, DEFAULT_AUTH_ACTOR_GROUPS_HEADER_PREFIX); - final String groupFilterPatternString = context.getInitParameter(GROUP_FILTER_PATTERN); - groupFilterPattern = groupFilterPatternString == null ? DEFAULT_GROUP_FILTER_PATTERN : Pattern.compile(groupFilterPatternString); + initialize(); } - private String getInitParameter(String paramName, String defaultValue) { - final String initParam = context.getInitParameter(paramName); - return initParam == null ? defaultValue : initParam; + @Override + HttpServletResponse getResponse() { + return response; } - @GET - public Response doGet() { - final Subject subject = SubjectUtils.getCurrentSubject(); - - final String primaryPrincipalName = subject == null ? null : SubjectUtils.getPrimaryPrincipalName(subject); - if (primaryPrincipalName == null) { - LOG.noPrincipalFound(); - return status(HttpServletResponse.SC_UNAUTHORIZED).build(); - } - response.setHeader(authHeaderActorIDName, primaryPrincipalName); - - // Populate actor groups headers - final Set matchingGroupNames = subject == null ? Collections.emptySet() - : SubjectUtils.getGroupPrincipals(subject).stream().filter(group -> groupFilterPattern.matcher(group.getName()).matches()).map(group -> group.getName()) - .collect(Collectors.toSet()); - if (!matchingGroupNames.isEmpty()) { - final List groupStrings = getGroupStrings(matchingGroupNames); - for (int i = 0; i < groupStrings.size(); i++) { - response.addHeader(String.format(Locale.ROOT, ACTOR_GROUPS_HEADER_FORMAT, authHeaderActorGroupsPrefix, i + 1), groupStrings.get(i)); - } - } - return ok().build(); + @Override + ServletContext getContext() { + return context; } - private List getGroupStrings(Collection groupNames) { - if (groupNames.isEmpty()) { - return Collections.emptyList(); - } - List groupStrings = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - for (String groupName : groupNames) { - if (sb.length() + groupName.length() > MAX_HEADER_LENGTH) { - groupStrings.add(sb.toString()); - sb = new StringBuilder(); - } - if (sb.length() > 0) { - sb.append(','); - } - sb.append(groupName); - } - if (sb.length() > 0) { - groupStrings.add(sb.toString()); - } - return groupStrings; + @GET + public Response doGet() { + return doGetImpl(); } - } \ No newline at end of file diff --git a/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java new file mode 100644 index 0000000000..ead516211c --- /dev/null +++ b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.knox.gateway.service.auth; + +import org.apache.knox.gateway.security.GroupPrincipal; +import org.apache.knox.gateway.security.PrimaryPrincipal; +import org.apache.knox.gateway.security.SubjectUtils; +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; + +import javax.security.auth.Subject; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.PathSegment; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class ExtAuthzResourceTest { + + private static final String USER_NAME = "test-username"; + private ServletContext context; + private HttpServletRequest request; + private HttpServletResponse response; + private final Subject subject = new Subject(); + + @Before + public void setUp() { + subject.getPrincipals().add(new PrimaryPrincipal(USER_NAME)); + } + + @Test + public void testIgnoreAdditionalPaths() throws Exception { + configureCommonExpectations(null, null, Collections.singleton("group1")); + final ExtAuthzResource extAuthzResource = new ExtAuthzResource(); + extAuthzResource.context = context; + extAuthzResource.response = response; + executeResourceWithAdditionalPath(extAuthzResource); + EasyMock.verify(response); + } + + + private void configureCommonExpectations(String actorIdHeaderName, String groupsHeaderPrefix, Collection groups) { + context = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(context.getInitParameter(ExtAuthzResource.AUTH_ACTOR_ID_HEADER_NAME)).andReturn(actorIdHeaderName).anyTimes(); + EasyMock.expect(context.getInitParameter(ExtAuthzResource.AUTH_ACTOR_GROUPS_HEADER_PREFIX)).andReturn(groupsHeaderPrefix).anyTimes(); + EasyMock.expect(context.getInitParameter(ExtAuthzResource.IGNORE_ADDITIONAL_PATH)).andReturn("true").anyTimes(); + request = EasyMock.createNiceMock(HttpServletRequest.class); + response = EasyMock.createNiceMock(HttpServletResponse.class); + + if (SubjectUtils.getPrimaryPrincipalName(subject) != null) { + final String expectedActorIdHeader = actorIdHeaderName == null ? "X-Knox-Actor-ID" : actorIdHeaderName; + response.setHeader(expectedActorIdHeader, USER_NAME); + EasyMock.expectLastCall(); + } + + if (!groups.isEmpty()) { + groups.forEach(group -> subject.getPrincipals().add(new GroupPrincipal(group))); + final int groupStringSize = calculateGroupStringSize(groups); + final int expectedGroupHeaderCount = groupStringSize / 1000 + 1; + final String expectedGroupsHeaderPrefix = (groupsHeaderPrefix == null ? "X-Knox-Actor-Groups" : groupsHeaderPrefix) + + "-"; + for (int i = 1; i <= expectedGroupHeaderCount; i++) { + response.addHeader(EasyMock.eq(expectedGroupsHeaderPrefix + i), EasyMock.anyString()); + EasyMock.expectLastCall(); + } + } + EasyMock.replay(context, request, response); + } + + private int calculateGroupStringSize(Collection groups) { + final AtomicInteger size = new AtomicInteger(0); + groups.forEach(group -> size.addAndGet(group.length())); + size.addAndGet(groups.size() - 1); // commas + return size.get(); + } + + + private Response executeResourceWithAdditionalPath(final ExtAuthzResource extAuthzResource) throws PrivilegedActionException { + return (Response) Subject.doAs(subject, new PrivilegedExceptionAction() { + + @Override + public Object run() throws Exception { + extAuthzResource.init(); + return extAuthzResource.doGetWithPath(new UriInfo() { + @Override + public String getPath() { + return "/gateway/sandbox/auth/api/v1/extauthz/does-not-exist"; + } + + @Override + public String getPath(boolean decode) { + return "/gateway/sandbox/auth/api/v1/extauthz/does-not-exist"; + } + + @Override + public List getPathSegments() { + return null; + } + + @Override + public List getPathSegments(boolean decode) { + return null; + } + + @Override + public URI getRequestUri() { + return null; + } + + @Override + public UriBuilder getRequestUriBuilder() { + return null; + } + + @Override + public URI getAbsolutePath() { + return null; + } + + @Override + public UriBuilder getAbsolutePathBuilder() { + return null; + } + + @Override + public URI getBaseUri() { + return null; + } + + @Override + public UriBuilder getBaseUriBuilder() { + return null; + } + + @Override + public MultivaluedMap getPathParameters() { + return null; + } + + @Override + public MultivaluedMap getPathParameters( + boolean decode) { + return null; + } + + @Override + public MultivaluedMap getQueryParameters() { + return null; + } + + @Override + public MultivaluedMap getQueryParameters( + boolean decode) { + return null; + } + + @Override + public List getMatchedURIs() { + return null; + } + + @Override + public List getMatchedURIs(boolean decode) { + return null; + } + + @Override + public List getMatchedResources() { + return null; + } + + @Override + public URI resolve(URI uri) { + return null; + } + + @Override + public URI relativize(URI uri) { + return null; + } + }); + } + + }); + } + + +}