Skip to content

Commit

Permalink
KNOX-2974 - Add a new endpoint 'extauthz' similar to pre that accepts…
Browse files Browse the repository at this point in the history
… HTTP verbs other than GET and if confgiured ignores additional context path params (#813)
  • Loading branch information
moresandeep authored Oct 30, 2023
1 parent 7a5189a commit 8e55969
Show file tree
Hide file tree
Showing 5 changed files with 492 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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<String> getGroupStrings(Collection<String> groupNames) {
if (groupNames.isEmpty()) {
return Collections.emptyList();
}
List<String> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,112 +17,41 @@
*/
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;
import javax.ws.rs.Path;
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<String> 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<String> 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<String> getGroupStrings(Collection<String> groupNames) {
if (groupNames.isEmpty()) {
return Collections.emptyList();
}
List<String> 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();
}

}
Loading

0 comments on commit 8e55969

Please sign in to comment.