← Files Dell Agentic WorkspaceARCHIVED FILE

scripts/authenticate_devspaces.py

10.5 KB · Nov 14, 2026 · 01:37 UTC

↓ Download file

#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2026 Dell Inc. or its subsidiaries. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

"""Native CLI authentication; standard library only, no credential logging."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import threading
from urllib.parse import urlsplit

sys.path.insert(0, str(Path(__file__).resolve().parent))
import devspaces_settings  # noqa: E402

CERTIFICATE_ERRORS = (
    'unable to verify the first certificate', 'unable to get local issuer certificate',
    'self signed certificate', 'self-signed certificate',
    'unable_to_verify_leaf_signature', 'unable_to_get_issuer_cert_locally',
)


def validate(connector, cluster):
    executable = shutil.which(connector)
    if not executable:
        raise ValueError('Provide an existing approved connector executable.')
    parsed = urlsplit(cluster)
    if (parsed.scheme != 'https' or not parsed.hostname or '@' in parsed.netloc
            or parsed.path not in ('', '/') or parsed.query or parsed.fragment):
        raise ValueError('Use a credential-free HTTPS cluster origin.')
    return str(Path(executable).resolve()), cluster.rstrip('/'), parsed.netloc


def child_environment(insecure):
    env = os.environ.copy()
    env.pop('NODE_TLS_REJECT_UNAUTHORIZED', None)
    ca_bundle = devspaces_settings.load().get('NODE_EXTRA_CA_CERTS')
    if ca_bundle:
        env.setdefault('NODE_EXTRA_CA_CERTS', ca_bundle)
    if insecure:
        env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'
    return env


def probe(connector, cluster_id, insecure=False):
    """Return sanitized access evidence from one CLI list.

    Certificate verification is never relaxed here. Only an explicit, administrator-set
    --allow-insecure-tls (passed in as insecure) runs unverified, and a certificate failure
    is reported as a failure rather than retried.
    """
    result = subprocess.run([connector, '--list'], stdin=subprocess.DEVNULL,
                            capture_output=True, text=True, encoding='utf-8', errors='replace',
                            timeout=30, env=child_environment(insecure))
    if result.returncode == 0:
        try:
            rows = json.loads(result.stdout)
        except (ValueError, TypeError):
            raise RuntimeError('Connector list returned invalid JSON; authentication not verified.') from None
        if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
            raise RuntimeError('Connector list returned an invalid workspace list.')
        verified = bool(rows) and all(row.get('clusterId') == cluster_id for row in rows)
        return {'list_accessible': True, 'cluster_verified': verified,
                'workspace_count': len(rows), 'tls_verification_enabled': not insecure}
    if any(marker in (result.stderr or '').lower() for marker in CERTIFICATE_ERRORS):
        raise RuntimeError('Certificate verification failed; configure a trusted CA with NODE_EXTRA_CA_CERTS.')
    # No raw output: the CLI can include credential-bearing URIs in errors.
    return {'list_accessible': False, 'cluster_verified': False, 'tls_verification_enabled': not insecure,
            'reason': 'CLI list failed; local authentication or connectivity needs attention.'}


def interactive_auth(connector, cluster, insecure):
    """Keep prompts on the native console; inspect stderr only for a TLS category.

    Lines are forwarded immediately to the local console, never retained or logged.
    Do not invoke interactive mode through a tool that captures the user's terminal.
    """
    process = subprocess.Popen([connector, '--auth', cluster], env=child_environment(insecure),
                               stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace')
    certificate = threading.Event()
    def forward_errors():
        try:
            for line in process.stderr:
                if any(marker in line.lower() for marker in CERTIFICATE_ERRORS):
                    certificate.set()
                sys.stderr.write(line)
                sys.stderr.flush()
        finally:
            process.stderr.close()
    reader = threading.Thread(target=forward_errors, daemon=True)
    reader.start()
    try:
        code = process.wait()
    except KeyboardInterrupt:
        if process.poll() is None:
            process.terminate()
            process.wait()
        raise
    finally:
        reader.join()
    return code, certificate.is_set()


def authenticate(connector, cluster, cluster_id, insecure=False, report=None):
    report = report or (lambda state, **fields: None)
    report('checking_access', tls_verification_enabled=not insecure)
    status = probe(connector, cluster_id, insecure)
    if status['cluster_verified']:
        print('Existing authentication verified; login skipped.', flush=True)
        print(json.dumps(status), flush=True)
        report('verified', **status)
        return 0
    print('Complete SSO in your machine browser and private authentication prompts in this terminal. Do not paste authentication URIs into chat.', flush=True)
    report('waiting_for_sso', tls_verification_enabled=not insecure)
    code, certificate_failure = interactive_auth(connector, cluster, insecure)
    if code:
        if certificate_failure:
            print('Authentication failed certificate verification. Configure a trusted CA with NODE_EXTRA_CA_CERTS; verification stays enabled.', flush=True)
        else:
            print('Authentication command failed or was interrupted; login not verified.', flush=True)
        report('failed', tls_verification_enabled=not insecure)
        return 1
    report('checking_access', tls_verification_enabled=not insecure)
    status = probe(connector, cluster_id, insecure)
    print(json.dumps(status), flush=True)
    if status['cluster_verified']:
        print('CLI authentication and cluster verified.', flush=True)
        report('verified', **status)
        return 0
    print('Authentication command ended; this does not confirm login. Empty inventory cannot verify the cluster. Return to the agent to check account and workspace access.', flush=True)
    report('unverified', **status)
    return 2


def publish_status(path, cluster, state, **fields):
    if path is None:
        return
    # Explicit allowlist: terminal content, credentials, and raw exceptions never
    # become machine-readable status. Native-window visibility is checked separately.
    allowed = {'list_accessible', 'cluster_verified', 'workspace_count', 'tls_verification_enabled'}
    value = {'state': state, 'cluster_url': cluster,
             **{key: item for key, item in fields.items() if key in allowed}}
    target = Path(path)
    temporary = target.with_suffix('.tmp')
    try:
        temporary.write_text(json.dumps(value), encoding='utf-8')
        temporary.replace(target)
    except OSError:
        # The owner can still detect an exited helper and recheck CLI access.
        pass


def main(argv=None):
    settings = devspaces_settings.load()
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--connector', default=settings.get('DEVSPACES_CONNECTOR_COMMAND', 'devspaces-cli-connector'))
    parser.add_argument('--cluster', default=settings.get('DEVSPACES_CLUSTER_URL', ''))
    parser.add_argument('--foreground', action='store_true', help='Use the existing local terminal')
    parser.add_argument('--check', action='store_true', help='Read-only CLI access check; never launch authentication')
    parser.add_argument('--pause', action='store_true', help='Keep the native terminal visible after completion')
    parser.add_argument('--status-file', type=Path, help='Private sanitized progress file for the owning MCP session')
    parser.add_argument('--allow-insecure-tls', action='store_true',
                        help='Administrator opt-in mirrored from DEVSPACES_ALLOW_INSECURE_TLS')
    args = parser.parse_args(argv)
    # The flag only mirrors the administrator's launch setting; on its own it cannot relax TLS.
    if args.allow_insecure_tls and not devspaces_settings.insecure_tls_allowed(settings, args.cluster):
        print('--allow-insecure-tls requires explicit approval bound to this cluster; verification stays enabled.', flush=True)
        args.allow_insecure_tls = False
    code = 1
    try:
        connector, cluster, cluster_id = validate(args.connector, args.cluster)
        if args.check:
            status = probe(connector, cluster_id, args.allow_insecure_tls)
            print(json.dumps(status), flush=True)
            return 0 if status['cluster_verified'] else 2
        if not args.foreground:
            if os.name != 'nt':
                raise RuntimeError('Use --foreground in an interactive local terminal on this platform.')
            command = [sys.executable, str(Path(__file__).resolve()), '--foreground', '--pause',
                       '--connector', connector, '--cluster', cluster]
            if args.allow_insecure_tls:
                command.append('--allow-insecure-tls')
            if args.status_file:
                command.extend(['--status-file', str(args.status_file.resolve())])
            process = subprocess.Popen(command, creationflags=subprocess.CREATE_NEW_CONSOLE,
                                       env=child_environment(False))
            print(json.dumps({'state': 'launched', 'pid': process.pid, 'browser': 'machine-default',
                              'authentication_verified': False}), flush=True)
            return 0
        code = authenticate(connector, cluster, cluster_id, args.allow_insecure_tls,
                            report=lambda state, **fields: publish_status(args.status_file, cluster, state, **fields))
    except KeyboardInterrupt:
        publish_status(args.status_file, args.cluster, 'cancelled')
        print('Authentication interrupted; login not verified.', flush=True)
    except (ValueError, OSError, RuntimeError, subprocess.TimeoutExpired):
        # Invalid URL input must never be written into status.
        publish_status(args.status_file, '', 'failed')
        # Never include raw exceptions, subprocess output, or URL input in diagnostics.
        print('Unable to verify CLI authentication. Check the connector path, credential-free HTTPS cluster origin, certificate trust, and local terminal.', flush=True)
    finally:
        if args.foreground and args.pause:
            try:
                input('Press Enter to close this window. ')
            except (EOFError, KeyboardInterrupt):
                pass
    return code


if __name__ == '__main__':
    raise SystemExit(main())

SHA-256: 806e63b7f554ea5efbfe0edd51bf1c1bc30c46c41fee7ce990d6f0ebef51cf44