Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@ def __init__(
default_user_remote_command_executor,
password,
scheduler_commands_factory,
alias=None,
):
self._default_user_remote_command_executor = default_user_remote_command_executor
self.cluster = cluster
self.scheduler = scheduler
self.user_num = user_num # TODO: don't need to keep this?
self.alias = f"PclusterUser{user_num}"
# Allow an explicit alias (e.g. AD names containing dots or longer than 8 chars). Fall
# back to the conventional PclusterUser<n> alias when not provided.
self.alias = alias or f"PclusterUser{user_num}"
self.home_dir = f"/home/{self.alias}"
self.ssh_keypair_path_prefix = str(test_datadir / self.alias)
self.ssh_private_key_path = self.ssh_keypair_path_prefix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,33 @@
from remote_command_executor import RemoteCommandExecutor
from retrying import retry
from time_utils import seconds
from utils import find_stack_by_tag, generate_stack_name, is_directory_supported, random_alphanumeric
from utils import (
find_stack_by_tag,
generate_stack_name,
get_cidr_from_ip,
get_local_ip,
is_directory_supported,
random_alphanumeric,
)
from xdist import get_xdist_worker_id

from tests.ad_integration.cluster_user import ClusterUser
from tests.common.dcv_common import check_dcv_session_authentication
from tests.common.utils import run_system_analyzer

NUM_USERS_TO_CREATE = 5
NUM_USERS_TO_TEST = 3

# DCV authenticator validates session ownership by matching the process UID. These AD users
# specifically exercise usernames that contain a dot and that exceed 8 characters (which a
# `ps aux` based lookup would truncate). They must be lowercase to satisfy the authenticator's
# USER_REGEX and stay within the 20-character sAMAccountName limit.
DCV_AD_USERS = ["pcluster.user", "pcluster.long.user"]

# DCV defaults to port 8443 and the external authenticator listens on the next port.
DCV_AUTHENTICATOR_PORT = 8444
DCV_SHARED_DIR = "/shared"


def get_infra_stack_outputs(stack_name):
cfn = boto3.client("cloudformation")
Expand Down Expand Up @@ -162,6 +180,9 @@ def _get_stack_parameters(directory_type, vpc_stack, keypair):
users = ""
for i in range(NUM_USERS_TO_CREATE):
users += f"PclusterUser{i},"
# Also provision the AD users used to validate DCV authentication with dotted/long names.
for dcv_user in DCV_AD_USERS:
users += f"{dcv_user},"

stack_parameters = [
{
Expand Down Expand Up @@ -603,6 +624,8 @@ def test_ad_integration( # noqa: C901

vpc = directory_stack_outputs.get("VpcId")
config_params.update(get_vpc_public_subnet(vpc))
local_ip = get_local_ip()
config_params.update({"dcv_access_from": get_cidr_from_ip(local_ip) if local_ip else "0.0.0.0/0"})

cluster_config = pcluster_config_reader(**config_params)
cluster = clusters_factory(cluster_config)
Expand Down Expand Up @@ -640,6 +663,12 @@ def test_ad_integration( # noqa: C901
)
shared_storage_mount_dirs = ["/shared"]
_run_user_workloads(users, test_datadir, shared_storage_mount_dirs)

# Validate DCV authentication for AD users whose names contain a dot and exceed 8 characters.
_check_dcv_authentication_for_ad_users(
cluster, scheduler, test_datadir, remote_command_executor, ad_user_password, scheduler_commands_factory
)

logging.info("Testing pcluster update and generate ssh keys for user")
_check_ssh_key_generation(users[0], remote_command_executor, scheduler_commands, False)

Expand Down Expand Up @@ -673,6 +702,30 @@ def test_ad_integration( # noqa: C901
run_system_analyzer(cluster, scheduler_commands_factory, request)


def _check_dcv_authentication_for_ad_users(
cluster, scheduler, test_datadir, remote_command_executor, ad_user_password, scheduler_commands_factory
):
"""Start a DCV session as each dotted/long AD user and verify the authenticator accepts it.

This exercises the DCV authenticator's session-ownership check (which resolves the username
to a numeric UID) against usernames that contain a dot and that are longer than 8 characters.
"""
for username in DCV_AD_USERS:
dcv_user = ClusterUser(
None,
test_datadir,
cluster,
scheduler,
remote_command_executor,
ad_user_password,
scheduler_commands_factory,
alias=username,
)
check_dcv_session_authentication(
dcv_user.remote_command_executor(), DCV_AUTHENTICATOR_PORT, DCV_SHARED_DIR, dcv_user.alias
)


def _check_ssh_auth(user, expect_success=True):
try:
user.ssh_connect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ HeadNode:
SubnetId: {{ public_subnet_id }}
Ssh:
KeyName: {{ key_name }}
Dcv:
Enabled: true
AllowedIps: {{ dcv_access_from }}
Imds:
Secured: {{ imds_secured }}
Scheduling:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ HeadNode:
SubnetId: {{ public_subnet_id }}
Ssh:
KeyName: {{ key_name }}
Dcv:
Enabled: true
AllowedIps: {{ dcv_access_from }}
Imds:
Secured: {{ imds_secured }}
Scheduling:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ HeadNode:
SubnetId: {{ public_subnet_id }}
Ssh:
KeyName: {{ key_name }}
Dcv:
Enabled: true
AllowedIps: {{ dcv_access_from }}
Imds:
Secured: {{ imds_secured }}
Scheduling:
Expand Down
61 changes: 61 additions & 0 deletions tests/integration-tests/tests/common/dcv_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# A copy of the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "LICENSE.txt" file accompanying this file.
# This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
# See the License for the specific language governing permissions and limitations under the License.
"""Shared helpers to launch DCV sessions and validate the DCV external authenticator."""

import logging
import re

from assertpy import assert_that

DCV_CONNECT_SCRIPT = "/opt/parallelcluster/scripts/pcluster_dcv_connect.sh"
DCV_SERVER_URL = "https://localhost"

# Output emitted by the DCV connect script, e.g.:
# PclusterDcvServerPort=8443 PclusterDcvSessionId=mysession PclusterDcvSessionToken=<token>
_DCV_SESSION_REGEX = r"PclusterDcvServerPort=([\d]+) PclusterDcvSessionId=([\w]+) PclusterDcvSessionToken=([\w-]+)"


def start_dcv_session(remote_command_executor, shared_dir):
"""Run the DCV connect script and return (server_port, session_id, session_token)."""
command_execution = remote_command_executor.run_remote_command(f"{DCV_CONNECT_SCRIPT} {shared_dir}")
dcv_parameters = re.search(_DCV_SESSION_REGEX, command_execution.stdout)
assert_that(dcv_parameters).described_as(
"Command '{0} {1}' failed, output: {2}, error: {3}".format(
DCV_CONNECT_SCRIPT, shared_dir, command_execution.stdout, command_execution.stderr
)
).is_not_none()
return dcv_parameters.group(1), dcv_parameters.group(2), dcv_parameters.group(3)


def assert_authenticator_accepts_session(
remote_command_executor, authenticator_port, session_id, session_token, username
):
"""Assert the DCV external authenticator validates the given session for the given username."""
response = remote_command_executor.run_remote_command(
f"curl -s -k {DCV_SERVER_URL}:{authenticator_port} "
f"-d sessionId={session_id} -d authenticationToken={session_token} -d clientAddr=someIp"
).stdout
assert_that(response).is_equal_to('<auth result="yes"><username>{0}</username></auth>'.format(username))


def check_dcv_session_authentication(remote_command_executor, authenticator_port, shared_dir, username):
"""Open a DCV session and verify the authenticator validates it for the given username.

This drives the full DCV authenticator path, including its session-ownership check that
resolves the username to a numeric UID.
"""
logging.info("Starting DCV session for user %s", username)
_, session_id, session_token = start_dcv_session(remote_command_executor, shared_dir)
logging.info("Verifying DCV authenticator validates the session for user %s", username)
assert_authenticator_accepts_session(
remote_command_executor, authenticator_port, session_id, session_token, username
)
44 changes: 16 additions & 28 deletions tests/integration-tests/tests/dcv/test_dcv.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
)

from tests.cloudwatch_logging.test_cloudwatch_logging import FeatureSpecificCloudWatchLoggingTestRunner
from tests.common.dcv_common import check_dcv_session_authentication

SERVER_URL = "https://localhost"
DCV_CONNECT_SCRIPT = "/opt/parallelcluster/scripts/pcluster_dcv_connect.sh"

# Crashes matching any of these patterns are never tolerated, regardless of TOLERATED_CRASH_PATTERNS.
UNTOLERATED_CRASH_PATTERNS = [
Expand Down Expand Up @@ -138,11 +138,15 @@ def _test_dcv_configuration(
),
(
"error cases (head node)",
lambda: _check_error_cases(head_node_remote_command_executor, dcv_authenticator_port),
lambda: _check_error_cases(
head_node_remote_command_executor, dcv_authenticator_port, get_username_for_os(os)
),
),
(
"error cases (login node)",
lambda: _check_error_cases(login_node_remote_command_executor, dcv_authenticator_port),
lambda: _check_error_cases(
login_node_remote_command_executor, dcv_authenticator_port, get_username_for_os(os)
),
),
("shared dir (head node)", lambda: _check_shared_dir(head_node_remote_command_executor, shared_dir)),
("shared dir (login node)", lambda: _check_shared_dir(login_node_remote_command_executor, shared_dir)),
Expand Down Expand Up @@ -191,16 +195,6 @@ def _check_shared_dir(remote_command_executor, shared_dir):
).is_greater_than(0)


def _check_auth_ok(remote_command_executor, external_authenticator_port, session_id, session_token, os):
username = get_username_for_os(os)
assert_that(
remote_command_executor.run_remote_command(
f"curl -s -k {SERVER_URL}:{external_authenticator_port} "
f"-d sessionId={session_id} -d authenticationToken={session_token} -d clientAddr=someIp"
).stdout
).is_equal_to('<auth result="yes"><username>{0}</username></auth>'.format(username))


def _get_crash_report(remote_command_executor):
"""Check for crash files on the node and return a crash report dictionary.

Expand Down Expand Up @@ -268,13 +262,19 @@ def _get_known_hosts_content(host_keys_file):
return b""


def _check_error_cases(remote_command_executor, dcv_authenticator_port):
def _check_error_cases(remote_command_executor, dcv_authenticator_port, username):
"""Check DCV errors for both head and login nodes."""
logging.info("Checking expected authentication failure on %s", remote_command_executor.target)
_check_auth_ko(
remote_command_executor,
dcv_authenticator_port,
"-d action=requestToken -d authUser=centos -d sessionID=invalidSessionId",
"The given user does not exist",
)
_check_auth_ko(
remote_command_executor,
dcv_authenticator_port,
f"-d action=requestToken -d authUser={username} -d sessionID=invalidSessionId",
"The given session does not exists",
)
_check_auth_ko(
Expand Down Expand Up @@ -350,18 +350,6 @@ def _test_show_url(cluster, region, dcv_port, access_from, use_login_node=False)

def _test_authenticator(remote_command_executor, dcv_authenticator_port, shared_dir, os):
"""Launch a DCV session and verify authenticator."""
command_execution = remote_command_executor.run_remote_command(f"{DCV_CONNECT_SCRIPT} {shared_dir}")
dcv_parameters = re.search(
r"PclusterDcvServerPort=([\d]+) PclusterDcvSessionId=([\w]+) PclusterDcvSessionToken=([\w-]+)",
command_execution.stdout,
check_dcv_session_authentication(
remote_command_executor, dcv_authenticator_port, shared_dir, get_username_for_os(os)
)
if dcv_parameters:
dcv_session_id = dcv_parameters.group(2)
dcv_session_token = dcv_parameters.group(3)
_check_auth_ok(remote_command_executor, dcv_authenticator_port, dcv_session_id, dcv_session_token, os)
else:
assert_that(dcv_parameters).described_as(
"Command '{0} {1}' fails, output: {2}, error: {3}".format(
DCV_CONNECT_SCRIPT, shared_dir, command_execution.stdout, command_execution.stderr
)
).is_not_none()
Loading