Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: respect authentication.skip.paths properly #9224

Merged
merged 3 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
* Handler that calls any authentication plugin
*/
public class AuthenticationPluginHandler implements Handler<RoutingContext> {
public static final Set<String> KSQL_AUTHENTICATION_SKIP_PATHS = ImmutableSet
private static final Set<String> KSQL_AUTHENTICATION_SKIP_PATHS = ImmutableSet
.of("/v1/metadata", "/v1/metadata/id", "/healthcheck");

private final Server server;
Expand All @@ -53,17 +53,9 @@ public AuthenticationPluginHandler(final Server server,
final AuthenticationPlugin securityHandlerPlugin) {
this.server = Objects.requireNonNull(server);
this.securityHandlerPlugin = Objects.requireNonNull(securityHandlerPlugin);
// We add in all the paths that don't require authentication/authorization from
// KsqlAuthorizationProviderHandler
final Set<String> unauthenticatedPaths = new HashSet<>(KSQL_AUTHENTICATION_SKIP_PATHS);
// And then we add anything from the property authentication.skip.paths
// This preserves the behaviour from the previous Jetty based implementation
final List<String> unauthed = server.getConfig()
.getList(KsqlRestConfig.AUTHENTICATION_SKIP_PATHS_CONFIG);
unauthenticatedPaths.addAll(unauthed);
final String paths = String.join(",", unauthenticatedPaths);
final String converted = convertCommaSeparatedWilcardsToRegex(paths);
unauthedPathsPattern = Pattern.compile(converted);
unauthedPathsPattern = getAuthorizationSkipPaths(unauthed);
}

@Override
Expand Down Expand Up @@ -91,6 +83,18 @@ public void handle(final RoutingContext routingContext) {
});
}

public static Pattern getAuthorizationSkipPaths(final List<String> unauthed) {
// We add in all the paths that don't require authentication/authorization from
// KsqlAuthorizationProviderHandler
final Set<String> unauthenticatedPaths = new HashSet<>(KSQL_AUTHENTICATION_SKIP_PATHS);
// And then we add anything from the property authentication.skip.paths
// This preserves the behaviour from the previous Jetty based implementation
unauthenticatedPaths.addAll(unauthed);
final String paths = String.join(",", unauthenticatedPaths);
final String converted = convertCommaSeparatedWilcardsToRegex(paths);
return Pattern.compile(converted);
}

private static class AuthPluginUser implements ApiUser {

private final Principal principal;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@

package io.confluent.ksql.api.auth;

import static io.confluent.ksql.api.auth.AuthenticationPluginHandler.KSQL_AUTHENTICATION_SKIP_PATHS;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;

import io.confluent.ksql.api.server.Server;
import io.confluent.ksql.rest.server.KsqlRestConfig;
import io.confluent.ksql.security.KsqlAuthorizationProvider;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.WorkerExecutor;
import io.vertx.ext.auth.User;
import io.vertx.ext.web.RoutingContext;
import java.util.regex.Pattern;

/**
* Handler that calls a KsqlAuthorizationProvider plugin that can be used for custom authorization
Expand All @@ -33,19 +35,22 @@ public class KsqlAuthorizationProviderHandler implements Handler<RoutingContext>

private final WorkerExecutor workerExecutor;
private final KsqlAuthorizationProvider ksqlAuthorizationProvider;
private final Pattern unauthenticatedpaths;

public KsqlAuthorizationProviderHandler(final WorkerExecutor workerExecutor,
public KsqlAuthorizationProviderHandler(final Server server,
final KsqlAuthorizationProvider ksqlAuthorizationProvider) {
this.workerExecutor = workerExecutor;
this.workerExecutor = server.getWorkerExecutor();
this.ksqlAuthorizationProvider = ksqlAuthorizationProvider;
this.unauthenticatedpaths = AuthenticationPluginHandler.getAuthorizationSkipPaths(
server.getConfig().getList(KsqlRestConfig.AUTHENTICATION_SKIP_PATHS_CONFIG));
}

@Override
public void handle(final RoutingContext routingContext) {

final String path = routingContext.normalisedPath();

if (KSQL_AUTHENTICATION_SKIP_PATHS.contains(path)) {
if (unauthenticatedpaths.matcher(path).matches()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, so AUTHENTICATION_SKIP_PATHS_CONFIG was not taken into account at all before.

routingContext.next();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ static void setupAuthHandlers(final Server server, final Router router) {
// For authorization use auth provider configured via security extension (if any)
securityExtension.getAuthorizationProvider()
.ifPresent(ksqlAuthorizationProvider -> router.route()
.handler(new KsqlAuthorizationProviderHandler(server.getWorkerExecutor(),
ksqlAuthorizationProvider)));
.handler(new KsqlAuthorizationProviderHandler(server, ksqlAuthorizationProvider)));

router.route().handler(AuthHandlers::resumeHandler);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Confluent Inc.
*
* Licensed under the Confluent Community License (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.confluent.io/confluent-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package io.confluent.ksql.api.auth;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.Test;

public class AuthenticationPluginHandlerTest {

@Test
public void shouldBuildRegexThatRespectsSkipPaths() {
// Given:
final List<String> configs = ImmutableList.of("/heartbeat", "/lag");

// When:
final Pattern skips = AuthenticationPluginHandler.getAuthorizationSkipPaths(configs);

// Then:
assertThat(skips.matcher("/heartbeat").matches(), is(true));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not also verify for /lags?
Could you also add a negative case, like /shouldnotbefound?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not also verify for /lags?

that doesn't add any coverage, I'm ensuring that what is added in the config is able to skip

Could you also add a negative case, like /shouldnotbefound?

Sure I'll add a negative test

Copy link
Member

@cadonna cadonna Jun 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that doesn't add any coverage, I'm ensuring that what is added in the config is able to skip

I cannot follow. Why does verifying for /lag not add any coverage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding /lag in the config list makes sure that the regex works when you have multiple components - I'm not sure why checking that it works for /lag adds any coverage beyond checking for /heartbeat (basically it uses the same exact code path to check for /lag as it does to check for /heartbeat)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional verification would check that the code works for all entries in the config. The code could not consider the other entry. Unit test should not only test the current code but they should also ensure that future refactorings of the code do not change the intended behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I can see your point :) I'll add it in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2020 Confluent Inc.
*
* Licensed under the Confluent Community License (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.confluent.io/confluent-community-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

package io.confluent.ksql.api.auth;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.confluent.ksql.api.server.Server;
import io.confluent.ksql.rest.server.KsqlRestConfig;
import io.confluent.ksql.security.KsqlAuthorizationProvider;
import io.vertx.core.WorkerExecutor;
import io.vertx.ext.web.RoutingContext;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class KsqlAuthorizationProviderHandlerTest {

@Mock
private Server server;
@Mock
private KsqlAuthorizationProvider authProvider;
@Mock
private WorkerExecutor workerExecutor;
@Mock
private RoutingContext routingContext;

@Test
public void shouldRespectServerAuthSkipPathsConfig() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a case where authentication is not skipped?

// Given:
Mockito.when(server.getWorkerExecutor()).thenReturn(workerExecutor);
Mockito.when(server.getConfig()).thenReturn(new KsqlRestConfig(
ImmutableMap.of(
KsqlRestConfig.AUTHENTICATION_SKIP_PATHS_CONFIG,
ImmutableList.of("/heartbeat")
)
));
Mockito.when(routingContext.normalisedPath()).thenReturn("/heartbeat");
KsqlAuthorizationProviderHandler handler = new KsqlAuthorizationProviderHandler(server, authProvider);

// When:
handler.handle(routingContext);

// Then (make sure the authorization "work" is skipped):
Mockito.verify(routingContext).next();
Mockito.verifyZeroInteractions(workerExecutor);
}

}