mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Connect: prepare for E2E testing (#64613)
* Allow storing `home`, `userData`, and `sessionData` under custom path * Optimize `getRuntimeSettings` loading sequence * Enable mocking webauthn in tests * Rename `webauthnmock_off.go` to `webauthnmock_nop.go`
This commit is contained in:
@@ -49,6 +49,9 @@ type Config struct {
|
||||
InstallationID string
|
||||
// AddKeysToAgent is passed to [client.Config].
|
||||
AddKeysToAgent string
|
||||
// WebauthnLogin allows tests to override the Webauthn Login func.
|
||||
// Defaults to wancli.Login.
|
||||
WebauthnLogin client.WebauthnLoginFunc
|
||||
// HardwareKeyAgent determines whether the daemon will run the hardware key agent.
|
||||
HardwareKeyAgent bool
|
||||
}
|
||||
@@ -92,5 +95,15 @@ func (c *Config) CheckAndSetDefaults() error {
|
||||
c.AddKeysToAgent = client.AddKeysToAgentAuto
|
||||
}
|
||||
|
||||
if c.WebauthnLogin == nil {
|
||||
// Enables WebAuthn mocking for E2E tests.
|
||||
// Requires the `webauthnmock` build tag and must never be enabled in production builds.
|
||||
webauthnLogin, err := webauthnLoginMock()
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
c.WebauthnLogin = webauthnLogin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ func Serve(ctx context.Context, cfg Config) error {
|
||||
Clock: clock,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
AddKeysToAgent: cfg.AddKeysToAgent,
|
||||
WebauthnLogin: cfg.WebauthnLogin,
|
||||
ClientStore: client.NewFSClientStore(cfg.HomeDir, client.WithHardwareKeyService(hwks)),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//go:build webauthnmock
|
||||
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package teleterm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
"github.com/gravitational/teleport/lib/auth/mocku2f"
|
||||
wancli "github.com/gravitational/teleport/lib/auth/webauthncli"
|
||||
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
|
||||
"github.com/gravitational/teleport/lib/client"
|
||||
)
|
||||
|
||||
const (
|
||||
privateKeyEnv = "E2E_WEBAUTHN_PRIVATE_KEY"
|
||||
credentialIDEnv = "E2E_WEBAUTHN_CREDENTIAL_ID"
|
||||
)
|
||||
|
||||
// In builds with the webauthnmock tag, provide a WebAuthn login mock for E2E tests.
|
||||
func webauthnLoginMock() (client.WebauthnLoginFunc, error) {
|
||||
privateKeyB64 := os.Getenv(privateKeyEnv)
|
||||
credentialIDB64 := os.Getenv(credentialIDEnv)
|
||||
|
||||
if privateKeyB64 == "" || credentialIDB64 == "" {
|
||||
return nil, trace.BadParameter("both %s and %s must be provided in webauthnmock mode", privateKeyEnv, credentialIDEnv)
|
||||
}
|
||||
|
||||
privateKeyPKCS8, err := base64.StdEncoding.DecodeString(privateKeyB64)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "decoding %s", privateKeyEnv)
|
||||
}
|
||||
credentialID, err := base64.StdEncoding.DecodeString(credentialIDB64)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "decoding %s", credentialIDEnv)
|
||||
}
|
||||
|
||||
keyAny, err := x509.ParsePKCS8PrivateKey(privateKeyPKCS8)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "parsing %s", privateKeyEnv)
|
||||
}
|
||||
privateKey, ok := keyAny.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, trace.BadParameter("%s must contain an ECDSA private key, got %T", privateKeyEnv, keyAny)
|
||||
}
|
||||
|
||||
device, err := mocku2f.CreateWithKeyHandle(credentialID)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "creating mock WebAuthn device")
|
||||
}
|
||||
device.PrivateKey = privateKey
|
||||
device.SetUV = true
|
||||
|
||||
return func(ctx context.Context, origin string, assertion *wantypes.CredentialAssertion, prompt wancli.LoginPrompt, opts *wancli.LoginOpts) (*proto.MFAAuthenticateResponse, string, error) {
|
||||
car, err := device.SignAssertion(origin, assertion)
|
||||
if err != nil {
|
||||
return nil, "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &proto.MFAAuthenticateResponse{
|
||||
Response: &proto.MFAAuthenticateResponse_Webauthn{
|
||||
Webauthn: wantypes.CredentialAssertionResponseToProto(car),
|
||||
},
|
||||
}, "", nil
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//go:build !webauthnmock
|
||||
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package teleterm
|
||||
|
||||
import "github.com/gravitational/teleport/lib/client"
|
||||
|
||||
// In builds without the webauthnmock tag (default), no WebAuthn login mock is provided.
|
||||
func webauthnLoginMock() (client.WebauthnLoginFunc, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -135,7 +135,12 @@ async function initializeApp(): Promise<void> {
|
||||
// TODO(gzdunek): DELETE IN 20.0.0. Users should already migrate to the new location.
|
||||
// Also remove TshHomeMigrationBanner component, relevant properties from app_state.json,
|
||||
// and address the TODO in teleport-connect.mdx > ##Troubleshooting.
|
||||
await migrateOldTshHomeOnce(logger, tshHome, appStateFileStorage);
|
||||
await migrateOldTshHomeOnce(
|
||||
logger,
|
||||
tshHome,
|
||||
appStateFileStorage,
|
||||
settings.userDataDir
|
||||
);
|
||||
|
||||
let mainProcess: MainProcess;
|
||||
try {
|
||||
@@ -371,9 +376,10 @@ function showDialogWithError(title: string, unknownError: unknown) {
|
||||
async function migrateOldTshHomeOnce(
|
||||
logger: Logger,
|
||||
tshHome: string,
|
||||
appStorage: FileStorage
|
||||
appStorage: FileStorage,
|
||||
userDataDir: string
|
||||
): Promise<void> {
|
||||
const oldTshHome = path.resolve(app.getPath('userData'), 'tsh');
|
||||
const oldTshHome = path.resolve(userDataDir, 'tsh');
|
||||
const tshHomeMigrationKey = 'tshHomeMigration';
|
||||
const tshMigration: TshHomeMigration =
|
||||
appStorage.get()?.[tshHomeMigrationKey];
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
@@ -32,6 +32,9 @@ const RESOURCES_PATH = app.isPackaged
|
||||
? process.resourcesPath
|
||||
: path.join(__dirname, '../../..');
|
||||
|
||||
// Optional root directory for app data; when set, Connect stores `home`, `userData`, and `sessionData` under this path.
|
||||
// Used in e2e tests.
|
||||
const CONNECT_DATA_DIR_ENV_VAR = 'CONNECT_DATA_DIR';
|
||||
const TSH_BIN_ENV_VAR = 'CONNECT_TSH_BIN_PATH';
|
||||
// __dirname of this file in dev mode is teleport/web/packages/teleterm/build/app/main
|
||||
// We default to teleport/build/tsh.
|
||||
@@ -57,37 +60,45 @@ const insecure =
|
||||
(dev && !!env.CONNECT_INSECURE);
|
||||
|
||||
export async function getRuntimeSettings(): Promise<RuntimeSettings> {
|
||||
const homeDir = app.getPath('home');
|
||||
const userDataDir = app.getPath('userData');
|
||||
const sessionDataDir = app.getPath('sessionData');
|
||||
const envDataDir = process.env[CONNECT_DATA_DIR_ENV_VAR];
|
||||
const homeDir = envDataDir
|
||||
? path.join(envDataDir, 'home')
|
||||
: app.getPath('home');
|
||||
const userDataDir = envDataDir
|
||||
? path.join(envDataDir, 'userData')
|
||||
: app.getPath('userData');
|
||||
const sessionDataDir = envDataDir
|
||||
? path.join(envDataDir, 'sessionData')
|
||||
: app.getPath('sessionData');
|
||||
const tempDataDir = app.getPath('temp');
|
||||
const {
|
||||
tsh: tshAddress,
|
||||
shared: sharedAddress,
|
||||
tshdEvents: tshdEventsAddress,
|
||||
} = await requestGrpcServerAddresses();
|
||||
const [grpcAddresses, kubeConfigsDir, certsDir, availableShells] =
|
||||
await Promise.all([
|
||||
requestGrpcServerAddresses(),
|
||||
getKubeConfigsDir(userDataDir),
|
||||
getCertsDir(userDataDir),
|
||||
getAvailableShells(),
|
||||
]);
|
||||
const { binDir, tshBinPath } = getBinaryPaths();
|
||||
const { username } = os.userInfo();
|
||||
const hostname = os.hostname();
|
||||
const kubeConfigsDir = getKubeConfigsDir();
|
||||
// TODO(ravicious): Replace with app.getPath('logs'). We started storing logs under a custom path.
|
||||
// Before switching to the recommended path, we need to investigate the impact of this change.
|
||||
// https://www.electronjs.org/docs/latest/api/app#appgetpathname
|
||||
const logsDir = path.join(userDataDir, 'logs');
|
||||
const installationId = loadInstallationId(
|
||||
path.resolve(app.getPath('userData'), 'installation_id')
|
||||
path.resolve(userDataDir, 'installation_id')
|
||||
);
|
||||
|
||||
const tshd = {
|
||||
binaryPath: tshBinPath,
|
||||
defaultHomeDir: path.resolve(app.getPath('home'), '.tsh'),
|
||||
requestedNetworkAddress: tshAddress,
|
||||
defaultHomeDir: path.resolve(homeDir, '.tsh'),
|
||||
requestedNetworkAddress: grpcAddresses.tsh,
|
||||
};
|
||||
const sharedProcess = {
|
||||
requestedNetworkAddress: sharedAddress,
|
||||
requestedNetworkAddress: grpcAddresses.shared,
|
||||
};
|
||||
const tshdEvents = {
|
||||
requestedNetworkAddress: tshdEventsAddress,
|
||||
requestedNetworkAddress: grpcAddresses.tshdEvents,
|
||||
};
|
||||
|
||||
// To start the app in dev mode, we run `electron path_to_main.js`. It means
|
||||
@@ -98,7 +109,6 @@ export async function getRuntimeSettings(): Promise<RuntimeSettings> {
|
||||
//
|
||||
// A workaround is to read the version from `process.env.npm_package_version`.
|
||||
const appVersion = dev ? process.env.npm_package_version : app.getVersion();
|
||||
const availableShells = await getAvailableShells();
|
||||
|
||||
return {
|
||||
dev,
|
||||
@@ -113,7 +123,7 @@ export async function getRuntimeSettings(): Promise<RuntimeSettings> {
|
||||
tempDataDir,
|
||||
binDir,
|
||||
agentBinaryPath: path.resolve(sessionDataDir, 'teleport', 'teleport'),
|
||||
certsDir: getCertsDir(),
|
||||
certsDir,
|
||||
availableShells,
|
||||
defaultOsShellId: getDefaultShell(availableShells),
|
||||
kubeConfigsDir,
|
||||
@@ -129,23 +139,16 @@ export async function getRuntimeSettings(): Promise<RuntimeSettings> {
|
||||
};
|
||||
}
|
||||
|
||||
function getCertsDir() {
|
||||
const certsPath = path.resolve(app.getPath('userData'), 'certs');
|
||||
if (!fs.existsSync(certsPath)) {
|
||||
fs.mkdirSync(certsPath);
|
||||
}
|
||||
if (fs.readdirSync(certsPath)) {
|
||||
fs.rmSync(certsPath, { force: true, recursive: true });
|
||||
fs.mkdirSync(certsPath);
|
||||
}
|
||||
async function getCertsDir(userDataDir: string): Promise<string> {
|
||||
const certsPath = path.resolve(userDataDir, 'certs');
|
||||
await fs.promises.rm(certsPath, { recursive: true, force: true });
|
||||
await fs.promises.mkdir(certsPath, { recursive: true });
|
||||
return certsPath;
|
||||
}
|
||||
|
||||
function getKubeConfigsDir(): string {
|
||||
const kubeConfigsPath = path.resolve(app.getPath('userData'), 'kube');
|
||||
if (!fs.existsSync(kubeConfigsPath)) {
|
||||
fs.mkdirSync(kubeConfigsPath);
|
||||
}
|
||||
async function getKubeConfigsDir(userDataDir: string): Promise<string> {
|
||||
const kubeConfigsPath = path.resolve(userDataDir, 'kube');
|
||||
await fs.promises.mkdir(kubeConfigsPath, { recursive: true });
|
||||
return kubeConfigsPath;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user