rpuch commented on code in PR #1569:
URL: https://github.com/apache/ignite-3/pull/1569#discussion_r1115514252
##########
modules/api/src/main/java/org/apache/ignite/IgnitionManager.java:
##########
@@ -137,22 +138,28 @@ public static void stop(String nodeName, @Nullable
ClassLoader clsLdr) {
* @param nodeName Name of the node that the initialization request will
be sent to.
* @param metaStorageNodeNames names of nodes that will host the Meta
Storage and the CMG.
* @param clusterName Human-readable name of the cluster.
+ * @param restAuthConfig REST authentication configuration thaw will be
applied after the initialization.
Review Comment:
```suggestion
* @param restAuthConfig REST authentication configuration that will be
applied after the initialization.
```
##########
modules/api/src/main/java/org/apache/ignite/rest/AuthProviderConfig.java:
##########
@@ -0,0 +1,25 @@
+/*
+ * 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.ignite.rest;
+
+/** General interface for all authentication providers. */
Review Comment:
It sseems to be an interface for configuration provider's configs, not for
configuration providers themselves
##########
modules/api/src/main/java/org/apache/ignite/rest/AuthType.java:
##########
@@ -0,0 +1,40 @@
+/*
+ * 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.ignite.rest;
+
+import java.util.Arrays;
+import org.jetbrains.annotations.Nullable;
+
+/** Authentication types. */
+public enum AuthType {
+ BASIC;
+
+ /**
+ * Parses {@link AuthType} from the given string.
+ *
+ * @param type string
+ * @return parsed {@link AuthType} or null
+ * */
Review Comment:
`*/` is usually placed on a line of its own
##########
modules/api/src/main/java/org/apache/ignite/IgnitionManager.java:
##########
@@ -162,29 +169,32 @@ public static synchronized void init(String nodeName,
Collection<String> metaSto
* @param metaStorageNodeNames names of nodes that will host the Meta
Storage.
* @param cmgNodeNames names of nodes that will host the CMG.
* @param clusterName Human-readable name of the cluster.
+ * @param restAuthConfig REST authentication configuration thaw will be
applied after the initialization.
Review Comment:
```suggestion
* @param restAuthConfig REST authentication configuration that will be
applied after the initialization.
```
##########
modules/api/src/main/java/org/apache/ignite/rest/AuthProviderConfig.java:
##########
@@ -0,0 +1,25 @@
+/*
+ * 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.ignite.rest;
+
+/** General interface for all authentication providers. */
+public interface AuthProviderConfig {
+ AuthType type();
Review Comment:
Please add javadocs for the methods
##########
modules/api/src/main/java/org/apache/ignite/rest/AuthType.java:
##########
@@ -0,0 +1,40 @@
+/*
+ * 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.ignite.rest;
+
+import java.util.Arrays;
+import org.jetbrains.annotations.Nullable;
+
+/** Authentication types. */
+public enum AuthType {
+ BASIC;
+
+ /**
+ * Parses {@link AuthType} from the given string.
+ *
+ * @param type string
+ * @return parsed {@link AuthType} or null
+ * */
+ @Nullable
Review Comment:
Is it useful to return a `null` here? Was an alternative to throw an
exception if we don't know such an auth type considered?
##########
modules/api/src/main/java/org/apache/ignite/rest/RestAuthConfig.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.ignite.rest;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Configuration of REST authentication. */
+public class RestAuthConfig {
+ private final boolean enabled;
+
+ private final List<AuthProviderConfig> providers;
+
+ /** Constructor. */
+ public RestAuthConfig(boolean enabled, List<AuthProviderConfig> providers)
{
+ if (providers == null) {
+ throw new IllegalArgumentException("providers cannot be null");
+ }
+
+ if (enabled && providers.isEmpty()) {
+ throw new IllegalArgumentException("providers cannot be empty, if
authentication is enabled");
+ }
+
+ this.enabled = enabled;
+ this.providers = providers;
+ }
+
+ public static RestAuthConfig disabledAuth() {
+ return new RestAuthConfig(false, Collections.emptyList());
+ }
+
+ public boolean enabled() {
Review Comment:
Please add javadoc explaining what it means that an auth config is enabled
##########
modules/api/src/main/java/org/apache/ignite/rest/RestAuthConfig.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.ignite.rest;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Configuration of REST authentication. */
+public class RestAuthConfig {
+ private final boolean enabled;
+
+ private final List<AuthProviderConfig> providers;
+
+ /** Constructor. */
+ public RestAuthConfig(boolean enabled, List<AuthProviderConfig> providers)
{
+ if (providers == null) {
+ throw new IllegalArgumentException("providers cannot be null");
+ }
+
+ if (enabled && providers.isEmpty()) {
+ throw new IllegalArgumentException("providers cannot be empty, if
authentication is enabled");
+ }
+
+ this.enabled = enabled;
+ this.providers = providers;
Review Comment:
Let's make a defensive copy (`this.providers = List.of(providers);`)
##########
modules/api/src/main/java/org/apache/ignite/rest/RestAuthConfig.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.ignite.rest;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Configuration of REST authentication. */
+public class RestAuthConfig {
+ private final boolean enabled;
+
+ private final List<AuthProviderConfig> providers;
+
+ /** Constructor. */
+ public RestAuthConfig(boolean enabled, List<AuthProviderConfig> providers)
{
+ if (providers == null) {
+ throw new IllegalArgumentException("providers cannot be null");
+ }
+
+ if (enabled && providers.isEmpty()) {
+ throw new IllegalArgumentException("providers cannot be empty, if
authentication is enabled");
+ }
+
+ this.enabled = enabled;
+ this.providers = providers;
+ }
+
+ public static RestAuthConfig disabledAuth() {
+ return new RestAuthConfig(false, Collections.emptyList());
+ }
+
+ public boolean enabled() {
+ return enabled;
+ }
+
+ public List<AuthProviderConfig> providers() {
Review Comment:
Pleaes add javadoc explaining what these providers do
##########
modules/cli/src/main/java/org/apache/ignite/internal/cli/call/cluster/ClusterInitCall.java:
##########
@@ -41,6 +42,7 @@ public DefaultCallOutput<String> execute(ClusterInitCallInput
input) {
.metaStorageNodes(input.getMetaStorageNodes())
.cmgNodes(input.getCmgNodes())
.clusterName(input.getClusterName())
+ .authConfig(new AuthConfig().enabled(false))
Review Comment:
Is it a safe thing to silently configure an insecure cluster by default?
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java:
##########
@@ -366,6 +390,36 @@ private void onElectedAsLeader(long term) {
LOG.warn("Error when executing onLeaderElected
callback", e);
}
});
+
+ raftServiceAfterJoin().thenCompose(service ->
service.readClusterState()
+ .thenCompose(state -> {
+ if (state == null) {
+ LOG.info("No CMG state found in the Raft storage");
+ return completedFuture(null);
+ } else if (state.restAuthToApply() == null) {
Review Comment:
How can it happen? Does it mean that the auth config has already been
successfully pushed to the distributed configuration? If yes, please add a
comment explaining this.
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java:
##########
@@ -366,6 +390,36 @@ private void onElectedAsLeader(long term) {
LOG.warn("Error when executing onLeaderElected
callback", e);
}
});
+
+ raftServiceAfterJoin().thenCompose(service ->
service.readClusterState()
+ .thenCompose(state -> {
+ if (state == null) {
+ LOG.info("No CMG state found in the Raft storage");
+ return completedFuture(null);
+ } else if (state.restAuthToApply() == null) {
+ LOG.info("No REST auth configuration found in the Raft
storage");
+ return completedFuture(null);
+ } else {
+ LOG.info("REST auth configuration found in the Raft
storage, going to apply it");
+ return
distributedConfigurationUpdater.updateRestAuthConfiguration(toRestAuthConfig(state.restAuthToApply()))
+ .thenCompose(unused -> {
+ ClusterState clusterState =
ClusterState.clusterState(
Review Comment:
Let's extract these lines to a method with an explaining name like
`removeAuthConfigFromClusterState()`
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/DistributedConfigurationUpdater.java:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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.ignite.internal.cluster.management;
+
+import java.util.concurrent.CompletableFuture;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.rest.configuration.AuthConfiguration;
+import org.apache.ignite.internal.rest.configuration.AuthProviderChange;
+import org.apache.ignite.internal.rest.configuration.BasicAuthProviderChange;
+import org.apache.ignite.internal.rest.configuration.ClusterRestConfiguration;
+import org.apache.ignite.rest.AuthProviderConfig;
+import org.apache.ignite.rest.AuthType;
+import org.apache.ignite.rest.BasicAuthProviderConfig;
+import org.apache.ignite.rest.RestAuthConfig;
+
+/**
+ * Updater is responsible for applying changes to the cluster configuration
when it's ready.
+ */
+public class DistributedConfigurationUpdater {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(DistributedConfigurationUpdater.class);
+
+ private final CompletableFuture<ClusterRestConfiguration>
clusterRestConfigurationFuture = new CompletableFuture<>();
+
+ public void setClusterRestConfiguration(ClusterRestConfiguration
clusterRestConfiguration) {
+ clusterRestConfigurationFuture.complete(clusterRestConfiguration);
Review Comment:
This future never gets completed with an exception, but during `IgniteImpl`
start it might happen that CMG manager already got this future and waits on it,
then Ignite start fails before distributed config manager is ready, so this
future never gets completed. How about completing it with an exception in the
very last `whenComplete()` in `IgniteImpl` (if there is an exception), to make
sure we let depending futures complete?
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java:
##########
@@ -366,6 +390,36 @@ private void onElectedAsLeader(long term) {
LOG.warn("Error when executing onLeaderElected
callback", e);
}
});
+
+ raftServiceAfterJoin().thenCompose(service ->
service.readClusterState()
+ .thenCompose(state -> {
+ if (state == null) {
+ LOG.info("No CMG state found in the Raft storage");
+ return completedFuture(null);
+ } else if (state.restAuthToApply() == null) {
+ LOG.info("No REST auth configuration found in the Raft
storage");
+ return completedFuture(null);
+ } else {
+ LOG.info("REST auth configuration found in the Raft
storage, going to apply it");
+ return
distributedConfigurationUpdater.updateRestAuthConfiguration(toRestAuthConfig(state.restAuthToApply()))
Review Comment:
Imagine that this code gets delayed. Do we take any measures to only allow
connections after auth is configured cluster-wide?
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/RestAuthConverter.java:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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.ignite.internal.cluster.management;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.cluster.management.network.auth.AuthProvider;
+import
org.apache.ignite.internal.cluster.management.network.auth.BasicAuthProvider;
+import org.apache.ignite.internal.cluster.management.network.auth.RestAuth;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.rest.AuthProviderConfig;
+import org.apache.ignite.rest.AuthType;
+import org.apache.ignite.rest.BasicAuthProviderConfig;
+import org.apache.ignite.rest.RestAuthConfig;
+import org.jetbrains.annotations.Nullable;
+
+/** Converter for {@link RestAuth}. */
+public class RestAuthConverter {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(DistributedConfigurationUpdater.class);
Review Comment:
The class name for which logger is obtained does not match current class
name. Is this intentional? It might make it harder to find the exact line that
produced a log entry.
##########
modules/cluster-management/src/test/java/org/apache/ignite/internal/cluster/management/raft/CmgRaftGroupListenerTest.java:
##########
@@ -146,6 +149,31 @@ void restoreFromSnapshotTriggersTopologyLeapEvent() {
verify(logicalTopology).fireTopologyLeap();
}
+ @Test
+ void writeAndReadRestAuth() {
Review Comment:
The test seems to make sure that auth config gets erased if cluster state is
written in an update without an auth config. I propose to change the method
name to highlight this, like `authConfiglessUpdateErasesAuthConfig()`
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementController.java:
##########
@@ -70,8 +85,14 @@ public CompletableFuture<Void> init(@Body InitCommand
initCommand) {
LOG.info("Received init command [metaStorageNodes={},
cmgNodes={}]", initCommand.metaStorageNodes(),
initCommand.cmgNodes());
}
-
- return clusterInitializer.initCluster(initCommand.metaStorageNodes(),
initCommand.cmgNodes(), initCommand.clusterName())
+ AuthConfigDto authConfigDto = initCommand.authConfig();
+ RestAuthConfig restAuthConfig = authConfigDto == null ? disabledAuth()
: authConfigDtoToRestAuthConfig(authConfigDto);
Review Comment:
When are we sent null as `authConfigDto`?
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/RestAuthConverter.java:
##########
@@ -0,0 +1,64 @@
+/*
+ * 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.ignite.internal.cluster.management;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.cluster.management.network.auth.AuthProvider;
+import
org.apache.ignite.internal.cluster.management.network.auth.BasicAuthProvider;
+import org.apache.ignite.internal.cluster.management.network.auth.RestAuth;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.rest.AuthProviderConfig;
+import org.apache.ignite.rest.AuthType;
+import org.apache.ignite.rest.BasicAuthProviderConfig;
+import org.apache.ignite.rest.RestAuthConfig;
+import org.jetbrains.annotations.Nullable;
+
+/** Converter for {@link RestAuth}. */
+public class RestAuthConverter {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(DistributedConfigurationUpdater.class);
+
+ /**
+ * Converts {@link RestAuth} to {@link RestAuthConfig}.
+ *
+ * @param restAuth Rest auth.
+ * @return Rest auth config.
+ */
+ public static RestAuthConfig toRestAuthConfig(RestAuth restAuth) {
+ List<AuthProviderConfig> providerConfigs = restAuth.providers()
+ .stream()
+ .map(RestAuthConverter::toAuthProviderConfig)
+ .collect(Collectors.toList());
+
+ return new RestAuthConfig(restAuth.enabled(), providerConfigs);
+ }
+
+ @Nullable
+ private static AuthProviderConfig toAuthProviderConfig(AuthProvider
authProvider) {
+ AuthType type = AuthType.parse(authProvider.type());
+ if (type == AuthType.BASIC) {
+ BasicAuthProvider basicAuthProvider = (BasicAuthProvider)
authProvider;
+ return new BasicAuthProviderConfig(basicAuthProvider.name(),
basicAuthProvider.login(), basicAuthProvider.password());
+ } else {
+ LOG.error("Unsupported authentication type: " +
authProvider.type());
+ return null;
Review Comment:
Should we fail instead? It would be scary to silently make a cluster
insecure because of a typo, like 'vasic' instead of 'basic', or even better,
change latin A to a similarly looking non-latin character.
##########
modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/ClusterManagementGroupManager.java:
##########
@@ -366,6 +390,36 @@ private void onElectedAsLeader(long term) {
LOG.warn("Error when executing onLeaderElected
callback", e);
}
});
+
+ raftServiceAfterJoin().thenCompose(service ->
service.readClusterState()
Review Comment:
Let's extract this block of code to a method of its own
(`pushAuthConfigToCluster()`?) as it seems to has an intent quiet different
from the other code in this method.
##########
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/auth/AuthProviderConfigDto.java:
##########
@@ -0,0 +1,37 @@
+/*
+ * 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.ignite.internal.rest.api.cluster.auth;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import org.apache.ignite.rest.AuthType;
+
+/**
+ * REST representation of {@link org.apache.ignite.rest.AuthProviderConfig}.
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = "type")
+@JsonSubTypes({
+ @JsonSubTypes.Type(value = BasicAuthProviderConfigDto.class, name =
"basic")
+})
+public interface AuthProviderConfigDto {
+ AuthType type();
Review Comment:
Please add javadocs to the methods
##########
gradle/libs.versions.toml:
##########
@@ -126,6 +128,10 @@ micronaut-http-core = { module =
"io.micronaut:micronaut-http", version.ref = "m
micronaut-http-server-core = { module = "io.micronaut:micronaut-http-server",
version.ref = "micronaut" }
micronaut-http-client = { module = "io.micronaut:micronaut-http-client",
version.ref = "micronaut" }
micronaut-http-server-netty = { module =
"io.micronaut:micronaut-http-server-netty", version.ref = "micronaut" }
+micronaut-security = { module = "io.micronaut.security:micronaut-security",
version.ref = "micronautSecurity" }
+micronaut-security-annotations = { module =
"io.micronaut.security:micronaut-security-annotations", version.ref =
"micronautSecurity" }
+micronaut-reactor = { module = "io.micronaut.reactor:micronaut-reactor",
version.ref = "micronautReactor" }
Review Comment:
Will we always shade reactor-core by putting it to own own package?
##########
modules/rest/src/main/java/org/apache/ignite/internal/rest/auth/AuthenticatorFactory.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import java.util.Objects;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.rest.configuration.AuthProviderView;
+import org.apache.ignite.internal.rest.configuration.BasicAuthProviderView;
+import org.apache.ignite.rest.AuthType;
+import org.jetbrains.annotations.Nullable;
+
+/** Factory for {@link Authenticator}. */
+class AuthenticatorFactory {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(AuthenticatorFactory.class);
+
+ @Nullable
+ static Authenticator create(AuthProviderView view) {
+ AuthType authType = AuthType.parse(view.type());
+ if (authType != null) {
+ return create(authType, view);
+ } else {
+ LOG.error("Unknown auth type: " + view);
Review Comment:
It seems safer to throw an exception here if the configuration is
misconfigured
##########
modules/rest-api/src/main/java/org/apache/ignite/internal/rest/exception/handler/ConversionErrorHandlerReplacement.java:
##########
@@ -0,0 +1,48 @@
+/*
+ * 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.ignite.internal.rest.exception.handler;
+
+import io.micronaut.context.annotation.Replaces;
+import io.micronaut.context.annotation.Requires;
+import io.micronaut.core.convert.exceptions.ConversionErrorException;
+import io.micronaut.http.HttpRequest;
+import io.micronaut.http.HttpResponse;
+import io.micronaut.http.server.exceptions.ConversionErrorHandler;
+import io.micronaut.http.server.exceptions.ExceptionHandler;
+import jakarta.inject.Singleton;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.HttpCode;
+import org.apache.ignite.internal.rest.problem.HttpProblemResponse;
+import org.apache.ignite.internal.util.ExceptionUtils;
+
+/**
+ * Replacement for {@link ConversionErrorHandlerReplacement}. Returns {@link
HttpProblemResponse}.
Review Comment:
```suggestion
* Replacement for {@link ConversionErrorHandler}. Returns {@link
HttpProblemResponse}.
```
##########
modules/rest/src/main/java/org/apache/ignite/internal/rest/auth/AuthenticatorFactory.java:
##########
@@ -0,0 +1,54 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import java.util.Objects;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.rest.configuration.AuthProviderView;
+import org.apache.ignite.internal.rest.configuration.BasicAuthProviderView;
+import org.apache.ignite.rest.AuthType;
+import org.jetbrains.annotations.Nullable;
+
+/** Factory for {@link Authenticator}. */
+class AuthenticatorFactory {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(AuthenticatorFactory.class);
+
+ @Nullable
+ static Authenticator create(AuthProviderView view) {
+ AuthType authType = AuthType.parse(view.type());
+ if (authType != null) {
+ return create(authType, view);
+ } else {
+ LOG.error("Unknown auth type: " + view);
+ return null;
+ }
+ }
+
+ @Nullable
+ private static Authenticator create(AuthType type, AuthProviderView view) {
+ if (Objects.requireNonNull(type) == AuthType.BASIC) {
+ BasicAuthProviderView basicAuthProviderView =
(BasicAuthProviderView) view;
+ return new BasicAuthenticator(basicAuthProviderView.login(),
basicAuthProviderView.password());
+ } else {
+ LOG.error("Couldn't create authenticator: " + view);
Review Comment:
Same thing: what's the advantage of returning a null?
##########
modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/AuthConfigurationValidatorImpl.java:
##########
@@ -0,0 +1,44 @@
+/*
+ * 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.ignite.internal.rest.configuration;
+
+import org.apache.ignite.configuration.validation.ValidationContext;
+import org.apache.ignite.configuration.validation.ValidationIssue;
+import org.apache.ignite.configuration.validation.Validator;
+
+/**
+ * Auth schema configuration validator implementation.
+ */
+public class AuthConfigurationValidatorImpl implements
Validator<AuthConfigurationValidator, AuthView> {
+
+ public static final AuthConfigurationValidatorImpl INSTANCE = new
AuthConfigurationValidatorImpl();
+
+ @Override
+ public void validate(AuthConfigurationValidator annotation,
ValidationContext<AuthView> ctx) {
+ AuthView view = ctx.getNewValue();
+
+ if (view == null) {
+ ctx.addIssue(new ValidationIssue(ctx.currentKey(), "Auth config
must not be null"));
+ return;
+ }
+
+ if (view.enabled() && view.providers().size() == 0) {
Review Comment:
If there are providers, but `enabled` is false, do we consider the config
valid?
##########
modules/rest/src/main/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProvider.java:
##########
@@ -0,0 +1,115 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import io.micronaut.http.HttpRequest;
+import io.micronaut.security.authentication.AuthenticationProvider;
+import io.micronaut.security.authentication.AuthenticationRequest;
+import io.micronaut.security.authentication.AuthenticationResponse;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.NamedListView;
+import org.apache.ignite.configuration.notifications.ConfigurationListener;
+import
org.apache.ignite.configuration.notifications.ConfigurationNotificationEvent;
+import org.apache.ignite.internal.logger.IgniteLogger;
+import org.apache.ignite.internal.logger.Loggers;
+import org.apache.ignite.internal.rest.configuration.AuthConfiguration;
+import org.apache.ignite.internal.rest.configuration.AuthProviderView;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.jetbrains.annotations.Nullable;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+
+/**
+ * Implementation of {@link AuthenticationProvider}. Creates a list of {@link
Authenticator} according to provided {@link AuthConfiguration}
+ * and updates them on configuration changes. Delegates {@link
AuthenticationRequest} to the list of {@link Authenticator}.
+ */
+public class DelegatingAuthenticationProvider implements
AuthenticationProvider, ConfigurationListener<AuthView> {
+
+ private static final IgniteLogger LOG =
Loggers.forClass(DelegatingAuthenticationProvider.class);
+
+ private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+ private final List<Authenticator> authenticators = new ArrayList<>();
+ private boolean authEnabled = false;
+
+ @Override
+ public Publisher<AuthenticationResponse> authenticate(HttpRequest<?>
httpRequest, AuthenticationRequest<?, ?> authenticationRequest) {
+ return Flux.create(emitter -> {
+ rwLock.readLock().lock();
+ try {
+ if (authEnabled) {
+ Optional<AuthenticationResponse> successResponse =
authenticators.stream()
+ .map(it -> it.authenticate(authenticationRequest))
+ .filter(AuthenticationResponse::isAuthenticated)
+ .findFirst();
+ if (successResponse.isPresent()) {
+ emitter.next(successResponse.get());
+ emitter.complete();
+ } else {
+ emitter.error(AuthenticationResponse.exception());
+ }
+ } else {
+ emitter.next(AuthenticationResponse.success("Unknown"));
+ emitter.complete();
+ }
+ } finally {
+ rwLock.readLock().unlock();
+ }
+ }, FluxSink.OverflowStrategy.ERROR);
+ }
+
+ @Override
+ public CompletableFuture<?>
onUpdate(ConfigurationNotificationEvent<AuthView> ctx) {
+ return CompletableFuture.runAsync(() ->
refreshProviders(ctx.newValue()));
+ }
+
+ private void refreshProviders(@Nullable AuthView view) {
+ rwLock.writeLock().lock();
+ try {
+ if (view == null || !view.enabled()) {
+ authEnabled = false;
+ authenticators.clear();
+ } else if (view.enabled() && view.providers().size() != 0) {
+ authenticators.clear();
+ authenticators.addAll(providersFromAuthView(view));
+ authEnabled = true;
+ } else {
+ LOG.error("Invalid configuration: auth enabled, but no
providers. Remain the old settings");
Review Comment:
```suggestion
LOG.error("Invalid configuration: auth enabled, but no
providers. Leave the old settings");
```
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
+ }
+
+
+ @Test
+ public void enableAuth() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+
+ UsernamePasswordCredentials invalidCredentials = new
UsernamePasswordCredentials("admin", "password");
+ authenticationProvider.authenticate(httpRequest,
invalidCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertThat(subscriber.lastError(),
is(instanceOf(AuthenticationException.class))),
+ () -> assertNull(subscriber.lastResponse())
+ );
+ }
+
+ @Test
+ public void enableAuthWithEmptyProviders() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true,
Collections.emptyList());
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials emptyCredentials = new
UsernamePasswordCredentials();
+ authenticationProvider.authenticate(httpRequest,
emptyCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+ }
+
+ @Test
+ public void disableAuth() {
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
Review Comment:
`enabledAuthView`?
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
Review Comment:
Let's move this to the field initializer (and make the field `final`)
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
+ }
+
+
+ @Test
+ public void enableAuth() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+
+ UsernamePasswordCredentials invalidCredentials = new
UsernamePasswordCredentials("admin", "password");
+ authenticationProvider.authenticate(httpRequest,
invalidCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertThat(subscriber.lastError(),
is(instanceOf(AuthenticationException.class))),
+ () -> assertNull(subscriber.lastResponse())
+ );
+ }
+
+ @Test
+ public void enableAuthWithEmptyProviders() {
Review Comment:
Is this a valid configuration? Auth is enabled, but noone to check the
credentials?
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
+ }
+
+
Review Comment:
A nit-pick: is this blank line intentional?
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
+ }
+
+
+ @Test
+ public void enableAuth() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+
+ UsernamePasswordCredentials invalidCredentials = new
UsernamePasswordCredentials("admin", "password");
+ authenticationProvider.authenticate(httpRequest,
invalidCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertThat(subscriber.lastError(),
is(instanceOf(AuthenticationException.class))),
+ () -> assertNull(subscriber.lastResponse())
+ );
+ }
+
+ @Test
+ public void enableAuthWithEmptyProviders() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true,
Collections.emptyList());
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials emptyCredentials = new
UsernamePasswordCredentials();
+ authenticationProvider.authenticate(httpRequest,
emptyCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+ }
+
+ @Test
+ public void disableAuth() {
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated()));
+
+ AuthView disableAuthView = new StubAuthView(false,
Collections.emptyList());
+ authenticationProvider.onUpdate(new StubAuthViewEvent(enableAuthView,
disableAuthView)).join();
+
+ UsernamePasswordCredentials emptyCredentials = new
UsernamePasswordCredentials();
+ authenticationProvider.authenticate(httpRequest,
emptyCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+ }
+
+ @Test
+ public void changedCredentials() {
+ AuthView adminAdminAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
adminAdminAuthView)).join();
+
+ UsernamePasswordCredentials adminAdminCredentials = new
UsernamePasswordCredentials("admin", "admin");
Review Comment:
How about changing the passwords from 'admin' and 'password' to
'old-password' and 'new-password', respectively? This will probably make it
easier to understand what's going on.
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ClusterRestConfigurationModuleTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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.ignite.internal.rest.configuration;
+
+import static
org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+class ClusterRestConfigurationModuleTest {
+
+ private final ClusterRestConfigurationModule module = new
ClusterRestConfigurationModule();
+
+ @Test
+ void typeIsDistributed() {
+ assertThat(module.type(), is(DISTRIBUTED));
+ }
+
+ @Test
+ void hasNoConfigurationRoots() {
Review Comment:
Test name does not correspond to the test logic
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/DelegatingAuthenticationProviderTest.java:
##########
@@ -0,0 +1,142 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.micronaut.http.HttpMethod;
+import io.micronaut.http.simple.SimpleHttpRequest;
+import io.micronaut.security.authentication.AuthenticationException;
+import io.micronaut.security.authentication.UsernamePasswordCredentials;
+import java.util.Collections;
+import org.apache.ignite.internal.rest.configuration.AuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthView;
+import org.apache.ignite.internal.rest.configuration.stub.StubAuthViewEvent;
+import
org.apache.ignite.internal.rest.configuration.stub.StubBasicAuthProviderView;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class DelegatingAuthenticationProviderTest {
+
+ private final SimpleHttpRequest<Object> httpRequest = new
SimpleHttpRequest<>(HttpMethod.GET, "/", null);
+
+ private DelegatingAuthenticationProvider authenticationProvider;
+
+ @BeforeEach
+ void setUp() {
+ authenticationProvider = new DelegatingAuthenticationProvider();
+ }
+
+
+ @Test
+ public void enableAuth() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+
+ UsernamePasswordCredentials invalidCredentials = new
UsernamePasswordCredentials("admin", "password");
+ authenticationProvider.authenticate(httpRequest,
invalidCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertThat(subscriber.lastError(),
is(instanceOf(AuthenticationException.class))),
+ () -> assertNull(subscriber.lastResponse())
+ );
+ }
+
+ @Test
+ public void enableAuthWithEmptyProviders() {
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+
+ AuthView enableAuthView = new StubAuthView(true,
Collections.emptyList());
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials emptyCredentials = new
UsernamePasswordCredentials();
+ authenticationProvider.authenticate(httpRequest,
emptyCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
+ () -> assertTrue(subscriber.lastResponse().isAuthenticated())
+ );
+ }
+
+ @Test
+ public void disableAuth() {
+ AuthView enableAuthView = new StubAuthView(true, new
StubBasicAuthProviderView("admin", "admin"));
+
+ authenticationProvider.onUpdate(new StubAuthViewEvent(null,
enableAuthView)).join();
+
+ UsernamePasswordCredentials validCredentials = new
UsernamePasswordCredentials("admin", "admin");
+ TestAuthSubscriber subscriber = new TestAuthSubscriber();
+ authenticationProvider.authenticate(httpRequest,
validCredentials).subscribe(subscriber);
+ assertAll(
+ () -> assertNull(subscriber.lastError()),
Review Comment:
The test seems to declare itself as testing auth disablement. Why would we
need to test for enablement here as well?
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/auth/TestAuthSubscriber.java:
##########
@@ -0,0 +1,66 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import io.micronaut.security.authentication.AuthenticationResponse;
+import org.jetbrains.annotations.Nullable;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+/** Implementation of {@link Subscriber} for tests. */
+public class TestAuthSubscriber implements Subscriber<AuthenticationResponse> {
+
+ private volatile Subscription source;
+
+ @Nullable
+ private volatile AuthenticationResponse lastResponse;
+
+ @Nullable
+ private volatile Throwable lastError;
+
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ source = subscription;
+ source.request(1);
+ }
+
+ @Override
+ public void onNext(AuthenticationResponse authenticationResponse) {
+ lastError = null;
+ lastResponse = authenticationResponse;
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ lastResponse = null;
+ lastError = throwable;
+ }
+
+ @Override
+ public void onComplete() {
+
+ }
+
+ public AuthenticationResponse lastResponse() {
Review Comment:
Probably the getters should also be annotated as `@Nullable`
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ClusterRestConfigurationModuleTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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.ignite.internal.rest.configuration;
+
+import static
org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+class ClusterRestConfigurationModuleTest {
+
+ private final ClusterRestConfigurationModule module = new
ClusterRestConfigurationModule();
+
+ @Test
+ void typeIsDistributed() {
+ assertThat(module.type(), is(DISTRIBUTED));
+ }
+
+ @Test
+ void hasNoConfigurationRoots() {
+ assertThat(module.rootKeys(), contains(ClusterRestConfiguration.KEY));
+ }
+
+ @Test
+ void providesNoValidators() {
Review Comment:
Test name has to be changed: it actually asserts that the expected
validators are provided
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ClusterRestConfigurationModuleTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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.ignite.internal.rest.configuration;
+
+import static
org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+class ClusterRestConfigurationModuleTest {
+
+ private final ClusterRestConfigurationModule module = new
ClusterRestConfigurationModule();
+
+ @Test
+ void typeIsDistributed() {
+ assertThat(module.type(), is(DISTRIBUTED));
+ }
+
+ @Test
+ void hasNoConfigurationRoots() {
+ assertThat(module.rootKeys(), contains(ClusterRestConfiguration.KEY));
+ }
+
+ @Test
+ void providesNoValidators() {
+ assertThat(module.validators(),
+ hasItems(
+ instanceOf(AuthConfigurationValidatorImpl.class),
+ instanceOf(AuthProvidersValidatorImpl.class))
+ );
+ }
+
+ @Test
+ void providesExternalTableConfigurationSchemaAsInternalExtension() {
Review Comment:
Test name is misleading
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/stub/StubAuthProviderListView.java:
##########
@@ -0,0 +1,60 @@
+/*
+ * 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.ignite.internal.rest.configuration.stub;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.ignite.configuration.NamedListView;
+import org.apache.ignite.internal.rest.configuration.AuthProviderView;
+import org.jetbrains.annotations.Nullable;
+
+/** Stub of {@link NamedListView} for tests. */
+public class StubAuthProviderListView implements
NamedListView<AuthProviderView> {
+
+ private final List<AuthProviderView> providers;
+
+ public StubAuthProviderListView(List<AuthProviderView> providers) {
+ this.providers = providers;
Review Comment:
How about making a defensive copy?
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
Review Comment:
If `toList()` is imported statically, this line starts to read naturally in
plain English: `collect(toList())`
##########
modules/rest/src/test/java/org/apache/ignite/internal/rest/configuration/ClusterRestConfigurationModuleTest.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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.ignite.internal.rest.configuration;
+
+import static
org.apache.ignite.configuration.annotation.ConfigurationType.DISTRIBUTED;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasItems;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+class ClusterRestConfigurationModuleTest {
+
+ private final ClusterRestConfigurationModule module = new
ClusterRestConfigurationModule();
+
+ @Test
+ void typeIsDistributed() {
+ assertThat(module.type(), is(DISTRIBUTED));
+ }
+
+ @Test
+ void hasNoConfigurationRoots() {
+ assertThat(module.rootKeys(), contains(ClusterRestConfiguration.KEY));
+ }
+
+ @Test
+ void providesNoValidators() {
+ assertThat(module.validators(),
+ hasItems(
+ instanceOf(AuthConfigurationValidatorImpl.class),
+ instanceOf(AuthProvidersValidatorImpl.class))
+ );
+ }
+
+ @Test
+ void providesExternalTableConfigurationSchemaAsInternalExtension() {
+ assertThat(module.internalSchemaExtensions(), is(empty()));
+ }
+
+ @Test
+ void providesNoPolymorphicSchemaExtensions() {
Review Comment:
Test name is misleading
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ nodes.forEach(RestNode::start);
+ }
+
+ @Test
+ public void disabledAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + metaStorageNode.name() + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": false\n"
+ + " }\n"
+ + "}";
+
+ URI uri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(uri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ URI clusterConfigUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/configuration/cluster/");
Review Comment:
If I understand correctly, this line and the remainder of the test just make
sure that authentication does not prevent us from executing any REST request.
If this is true, please extract these lines to a method like
'assertThatRestRequestsAreAllowed()` (or something like that) to make the
tests' core logic easier to see.
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ nodes.forEach(RestNode::start);
+ }
+
+ @Test
+ public void disabledAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + metaStorageNode.name() + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": false\n"
+ + " }\n"
+ + "}";
+
+ URI uri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
Review Comment:
Let's extract a method that inits the cluster as it's used by all tests. So
many secondary details distract attention from the test logic, they should be
hidden away.
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ nodes.forEach(RestNode::start);
+ }
+
+ @Test
+ public void disabledAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + metaStorageNode.name() + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": false\n"
+ + " }\n"
+ + "}";
+
+ URI uri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(uri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ URI clusterConfigUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .build();
+
+ HttpResponse<String> httpResponse = client.send(clusterConfigRequest,
BodyHandlers.ofString());
+ assertEquals(200, httpResponse.statusCode());
+ }
+
+ @Test
+ public void basicAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String msNodes = nodes.stream()
+ .map(it -> it.name())
+ .map(name -> "\"" + name + "\"")
+ .collect(Collectors.joining(","));
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + msNodes
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": true,\n"
+ + " \"providers\": [\n"
+ + " {\n"
+ + " \"name\": \"basic\",\n"
+ + " \"type\": \"basic\",\n"
+ + " \"login\": \"admin\",\n"
+ + " \"password\": \"admin\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " }";
+
+ URI clusterInitUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(clusterInitUri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri).build();
+ assertTrue(waitForCondition(() -> {
+ try {
+ return client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode() == 401;
+ } catch (Exception e) {
+ return false;
+ }
+ }, Duration.ofSeconds(5).toMillis()));
+ }
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "admin"))
+ .build();
+ assertEquals(200, client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode());
+ }
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "password"))
Review Comment:
Let's change 'password' to 'wrong-password' to make it speak for itself
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ nodes.forEach(RestNode::start);
+ }
+
+ @Test
+ public void disabledAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + metaStorageNode.name() + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": false\n"
+ + " }\n"
+ + "}";
+
+ URI uri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(uri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ URI clusterConfigUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .build();
+
+ HttpResponse<String> httpResponse = client.send(clusterConfigRequest,
BodyHandlers.ofString());
+ assertEquals(200, httpResponse.statusCode());
+ }
+
+ @Test
+ public void basicAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String msNodes = nodes.stream()
+ .map(it -> it.name())
+ .map(name -> "\"" + name + "\"")
+ .collect(Collectors.joining(","));
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + msNodes
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": true,\n"
+ + " \"providers\": [\n"
+ + " {\n"
+ + " \"name\": \"basic\",\n"
+ + " \"type\": \"basic\",\n"
+ + " \"login\": \"admin\",\n"
+ + " \"password\": \"admin\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " }";
+
+ URI clusterInitUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(clusterInitUri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri).build();
+ assertTrue(waitForCondition(() -> {
+ try {
+ return client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode() == 401;
+ } catch (Exception e) {
+ return false;
+ }
+ }, Duration.ofSeconds(5).toMillis()));
+ }
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "admin"))
+ .build();
+ assertEquals(200, client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode());
+ }
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "password"))
+ .build();
+ assertEquals(401, client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode());
+ }
+
+ String updateRestAuthConfigBody = "{\n"
+ + " \"rest\": {\n"
+ + " \"authConfiguration\": {\n"
+ + " \"enabled\": true,\n"
+ + " \"providers\": [\n"
+ + " {\n"
+ + " \"name\": \"basic\",\n"
+ + " \"type\": \"basic\",\n"
+ + " \"login\": \"admin\",\n"
+ + " \"password\": \"password\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " }\n"
+ + "}";
+
+ URI updateClusterConfigUri = URI.create(metaStorageNode.httpAddress()
+ "/management/v1/configuration/cluster/");
+ HttpRequest updateClusterConfigRequest =
HttpRequest.newBuilder(updateClusterConfigUri)
+ .header("content-type", "text/plain")
+ .header("Authorization", basicAuthenticationHeader("admin",
"admin"))
+ .method("PATCH",
BodyPublishers.ofString(updateRestAuthConfigBody))
+ .build();
+
+ HttpResponse<String> updateClusterConfigResponse =
client.send(updateClusterConfigRequest, BodyHandlers.ofString());
+ assertThat(updateClusterConfigResponse.statusCode(), is(200));
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "admin"))
+ .build();
+
+ assertTrue(waitForCondition(() -> {
+ try {
+ return client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode() == 401;
+ } catch (Exception e) {
+ return false;
+ }
+ }, Duration.ofSeconds(5).toMillis()));
+ }
+
+ for (RestNode node : nodes) {
+ URI clusterConfigUri = URI.create(node.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .header("Authorization",
basicAuthenticationHeader("admin", "password"))
+ .build();
+ assertEquals(200, client.send(clusterConfigRequest,
BodyHandlers.ofString()).statusCode());
+ }
+ }
+
+ @AfterEach
+ void tearDown() {
+ nodes.forEach(RestNode::stop);
+ }
+
+ private static void checkAllNodesStarted(List<RestNode> nodes) {
Review Comment:
It seems that the main purpose of this method is to wait till all nodes
started, not just check that they are already started. I suggest to rename it,
like `waitForAllNodesStarted()`
##########
modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/auth/ItAuthRestTest.java:
##########
@@ -0,0 +1,250 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static
org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition;
+import static
org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.apache.ignite.internal.rest.RestNode;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+
+/** Tests for the REST authentication configuration. */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItAuthRestTest {
+
+ /** HTTP client that is expected to be defined in subclasses. */
+ private HttpClient client;
+
+ private List<RestNode> nodes;
+
+ @WorkDirectory
+ private Path workDir;
+
+ @BeforeEach
+ void setUp(TestInfo testInfo) {
+ client = HttpClient.newBuilder()
+ .build();
+
+ nodes = IntStream.range(0, 3)
+ .mapToObj(id -> {
+ return RestNode.builder()
+ .setWorkDir(workDir)
+ .setName(testNodeName(testInfo, id))
+ .setNetworkPort(3344 + id)
+ .setHttpPort(10300 + id)
+ .setHttpsPort(10400 + id)
+ .setSslEnabled(false)
+ .setDualProtocol(false)
+ .build();
+ })
+ .collect(Collectors.toList());
+
+ nodes.forEach(RestNode::start);
+ }
+
+ @Test
+ public void disabledAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + " \"" + metaStorageNode.name() + "\"\n"
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": false\n"
+ + " }\n"
+ + "}";
+
+ URI uri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(uri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ URI clusterConfigUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/configuration/cluster/");
+ HttpRequest clusterConfigRequest =
HttpRequest.newBuilder(clusterConfigUri)
+ .build();
+
+ HttpResponse<String> httpResponse = client.send(clusterConfigRequest,
BodyHandlers.ofString());
+ assertEquals(200, httpResponse.statusCode());
+ }
+
+ @Test
+ public void basicAuthentication(TestInfo testInfo) throws IOException,
InterruptedException {
+ RestNode metaStorageNode = nodes.get(0);
+
+ String msNodes = nodes.stream()
+ .map(it -> it.name())
+ .map(name -> "\"" + name + "\"")
+ .collect(Collectors.joining(","));
+
+ String initClusterBody = "{\n"
+ + " \"metaStorageNodes\": [\n"
+ + msNodes
+ + " ],\n"
+ + " \"cmgNodes\": [],\n"
+ + " \"clusterName\": \"cluster\",\n"
+ + " \"authConfig\": {\n"
+ + " \"enabled\": true,\n"
+ + " \"providers\": [\n"
+ + " {\n"
+ + " \"name\": \"basic\",\n"
+ + " \"type\": \"basic\",\n"
+ + " \"login\": \"admin\",\n"
+ + " \"password\": \"admin\"\n"
+ + " }\n"
+ + " ]\n"
+ + " }\n"
+ + " }";
+
+ URI clusterInitUri = URI.create(metaStorageNode.httpAddress() +
"/management/v1/cluster/init");
+ HttpRequest initRequest = HttpRequest.newBuilder(clusterInitUri)
+ .header("content-type", "application/json")
+ .POST(BodyPublishers.ofString(initClusterBody))
+ .build();
+
+ HttpResponse<String> initResponse = client.send(initRequest,
BodyHandlers.ofString());
+ assertThat(initResponse.statusCode(), is(200));
+ checkAllNodesStarted(nodes);
+
+ for (RestNode node : nodes) {
Review Comment:
We should not have such long methods. It seems to perfectly break into
consecutive blocks like 'configure BASIC auth', 'wait till auth config is
applied', 'check with correct password', 'check with wrong password', etc.
Please extract the corresponding methods to make it easier to work with this
method.
##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -499,16 +508,21 @@ public class IgniteImpl implements Ignite {
}
private RestComponent createRestComponent(String name) {
+ AuthConfiguration authConfiguration =
clusterCfgMgr.configurationRegistry()
Review Comment:
It would be nice to break this method into blocks using blank lines
##########
modules/runner/src/main/java/org/apache/ignite/internal/app/IgniteImpl.java:
##########
@@ -263,6 +268,9 @@ public class IgniteImpl implements Ignite {
private final ScheduledExecutorService raftExecutorService;
+ /** CompletableFuture of {@link ClusterRestConfiguration} that will be
resolved after the cluster initialization. */
Review Comment:
It is not a `CompletableFuture`, it probably contains the future. Looks like
this comment needs to be refined.
##########
modules/cluster-management/build.gradle:
##########
@@ -35,6 +35,7 @@ dependencies {
implementation project(':ignite-raft-api')
implementation project(':ignite-vault')
implementation project(':ignite-rocksdb-common')
+ implementation project(':ignite-rest')
Review Comment:
Why is it needed here?
##########
modules/api/src/main/java/org/apache/ignite/Ignition.java:
##########
@@ -168,8 +170,15 @@ CompletableFuture<Ignite> start(
* @param metaStorageNodeNames Names of nodes that will host the Meta
Storage.
* @param cmgNodeNames Names of nodes that will host the CMG.
* @param clusterName Human-readable name of the cluster.
+ * @param restAuthConfig REST authentication configuration.
* @throws IgniteException If the given node has not been started or has
been stopped.
* @see <a
href="https://cwiki.apache.org/confluence/display/IGNITE/IEP-77%3A+Node+Join+Protocol+and+Initialization+for+Ignite+3">IEP-77</a>
*/
- void init(String nodeName, Collection<String> metaStorageNodeNames,
Collection<String> cmgNodeNames, String clusterName);
+ void init(
+ String nodeName,
+ Collection<String> metaStorageNodeNames,
+ Collection<String> cmgNodeNames,
+ String clusterName,
+ RestAuthConfig restAuthConfig
Review Comment:
Shouldn't we have overloads without `RestAuthConfig` which init an insecured
cluster? This should be probably coordinated with the guys responsible for our
public API.
##########
modules/rest/src/main/java/org/apache/ignite/internal/rest/auth/Authenticator.java:
##########
@@ -0,0 +1,25 @@
+/*
+ * 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.ignite.internal.rest.auth;
+
+import io.micronaut.security.authentication.AuthenticationRequest;
+import io.micronaut.security.authentication.AuthenticationResponse;
+
+interface Authenticator {
Review Comment:
The method still does not seem to have a javadoc on it. Am I looking in a
wrong direction?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]