Skip to content

Commit

Permalink
Merge pull request #2350 from posit-dev/sagerb-support-insecure-tls
Browse files Browse the repository at this point in the history
Add support for insecure TLS connections to Connect Servers
  • Loading branch information
sagerb authored Oct 4, 2024
2 parents 14c2dd7 + 3560007 commit 51a563a
Show file tree
Hide file tree
Showing 17 changed files with 132 additions and 31 deletions.
3 changes: 2 additions & 1 deletion cmd/publisher/commands/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type DeployCmd struct {
SaveName string `name:"name" short:"n" help:"Save deployment with this name (in .posit/deployments/)"`
Account *accounts.Account `kong:"-"`
Config *config.Config `kong:"-"`
// NOTE: Currently hardcoded to insecure = false. No CLI param added for now.
}

func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error {
Expand Down Expand Up @@ -56,7 +57,7 @@ func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext)
if err != nil {
return err
}
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts, nil)
stateStore, err := state.New(absPath, cmd.AccountName, cmd.ConfigName, "", cmd.SaveName, ctx.Accounts, nil, false)
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/publisher/commands/redeploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type RedeployCmd struct {
ConfigName string `name:"config" short:"c" help:"Configuration name (in .posit/publish/)"`
Config *config.Config `kong:"-"`
Target *deployment.Deployment `kong:"-"`
// NOTE: Currently hardcoded to insecure = false. No CLI param added for now.
}

func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) error {
Expand All @@ -41,7 +42,7 @@ func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContex
if err != nil {
return fmt.Errorf("invalid deployment name '%s': %w", cmd.TargetName, err)
}
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts, nil)
stateStore, err := state.New(absPath, "", cmd.ConfigName, cmd.TargetName, "", ctx.Accounts, nil, false)
if err != nil {
return err
}
Expand Down
12 changes: 12 additions & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
],
"main": "./dist/extension.js",
"contributes": {
"configuration": [
{
"title": "Posit Publisher",
"properties": {
"positPublisher.verifyCertificates": {
"markdownDescription": "Verify TLS certificates for connections. Only **disable** this setting if you experience certificate verification errors and the Posit Connect server is using a self-signed, expired, or otherwise misconfigured certificate.",
"type": "boolean",
"default": true
}
}
}
],
"commands": [
{
"command": "posit.publisher.init-project",
Expand Down
2 changes: 2 additions & 0 deletions extensions/vscode/src/api/resources/ContentRecords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,15 @@ export class ContentRecords {
targetName: string,
accountName: string,
configName: string,
insecure: boolean,
dir: string,
secrets?: Record<string, string>,
) {
const data = {
account: accountName,
config: configName,
secrets: secrets,
insecure: insecure,
};
const encodedTarget = encodeURIComponent(targetName);
return this.client.post<{ localId: string }>(
Expand Down
3 changes: 2 additions & 1 deletion extensions/vscode/src/api/resources/Credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ export class Credentials {
// for valid URL and invalid API key: no user, error in TestResult
// indicating that the API key is invalid
// 404 - Agent not found...
test(url: string, apiKey?: string) {
test(url: string, insecure: boolean, apiKey?: string) {
return this.client.post<TestResult>(`test-credentials`, {
url,
apiKey,
insecure,
});
}
}
7 changes: 6 additions & 1 deletion extensions/vscode/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import * as path from "path";

import { ExtensionContext } from "vscode";
import { ExtensionContext, Uri } from "vscode";

import { HOST } from "src";

Expand All @@ -19,3 +19,8 @@ export const create = async (
const getExecutableBinary = (context: ExtensionContext): string => {
return path.join(context.extensionPath, "bin", "publisher");
};

const args = ["@ext:posit.publisher"];
export const openConfigurationCommand = Uri.parse(
`command:workbench.action.openSettings?${encodeURIComponent(JSON.stringify(args))}`,
);
12 changes: 11 additions & 1 deletion extensions/vscode/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright (C) 2024 by Posit Software, PBC.

import { ExtensionContext, Uri, commands } from "vscode";
import { ExtensionContext, Uri, commands, workspace } from "vscode";

import * as ports from "src/ports";
import { Service } from "src/services";
Expand Down Expand Up @@ -97,3 +97,13 @@ export async function deactivate() {
await service.stop();
}
}

export const extensionSettings = {
verifyCertificates(): boolean {
// set value from extension configuration - defaults to true
const configuration = workspace.getConfiguration("positPublisher");
const value: boolean | undefined =
configuration.get<boolean>("verifyCertificates");
return value !== undefined ? value : true;
},
};
25 changes: 20 additions & 5 deletions extensions/vscode/src/multiStepInputs/newCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { formatURL, normalizeURL } from "src/utils/url";
import { checkSyntaxApiKey } from "src/utils/apiKeys";
import { showProgress } from "src/utils/progress";
import { openConfigurationCommand } from "src/commands";
import { extensionSettings } from "src/extension";

const createNewCredentialLabel = "Create a New Credential";

Expand Down Expand Up @@ -77,7 +79,6 @@ export async function newCredential(
typeof state.data.url === "string" && state.data.url.length
? state.data.url
: "";

const url = await input.showInputBox({
title: state.title,
step: thisStepNumber,
Expand Down Expand Up @@ -123,16 +124,26 @@ export async function newCredential(
});
}
try {
const testResult = await api.credentials.test(input);
const testResult = await api.credentials.test(
input,
!extensionSettings.verifyCertificates(), // insecure = !verifyCertificates
);
if (testResult.status !== 200) {
return Promise.resolve({
message: `Error: Invalid URL (unable to validate connectivity with Server URL - API Call result: ${testResult.status} - ${testResult.statusText}).`,
severity: InputBoxValidationSeverity.Error,
});
}
if (testResult.data.error) {
const err = testResult.data.error;
if (err) {
if (err.code === "errorCertificateVerification") {
return Promise.resolve({
message: `Error: URL Not Accessible - ${err.msg}. If applicable, consider disabling [Verify TLS Certificates](${openConfigurationCommand}).`,
severity: InputBoxValidationSeverity.Error,
});
}
return Promise.resolve({
message: `Error: Invalid URL (${testResult.data.error.msg}).`,
message: `Error: Invalid URL (unable to validate connectivity with Server URL - ${getMessageFromError(err)}).`,
severity: InputBoxValidationSeverity.Error,
});
}
Expand Down Expand Up @@ -196,7 +207,11 @@ export async function newCredential(
const serverUrl =
typeof state.data.url === "string" ? state.data.url : "";
try {
const testResult = await api.credentials.test(serverUrl, input);
const testResult = await api.credentials.test(
serverUrl,
!extensionSettings.verifyCertificates(), // insecure = !verifyCertificates
input,
);
if (testResult.status !== 200) {
return Promise.resolve({
message: `Error: Invalid API Key (unable to validate API Key - API Call result: ${testResult.status} - ${testResult.statusText}).`,
Expand Down
12 changes: 10 additions & 2 deletions extensions/vscode/src/multiStepInputs/newDeployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { DeploymentObjects } from "src/types/shared";
import { showProgress } from "src/utils/progress";
import { relativeDir, relativePath, vscodeOpenFiles } from "src/utils/files";
import { ENTRYPOINT_FILE_EXTENSIONS } from "src/constants";
import { extensionSettings } from "src/extension";

export async function newDeployment(
viewId: string,
Expand Down Expand Up @@ -570,7 +571,10 @@ export async function newDeployment(
});
}
try {
const testResult = await api.credentials.test(input);
const testResult = await api.credentials.test(
input,
!extensionSettings.verifyCertificates(), // insecure = !verifyCertificates
);
if (testResult.status !== 200) {
return Promise.resolve({
message: `Error: Invalid URL (unable to validate connectivity with Server URL - API Call result: ${testResult.status} - ${testResult.statusText}).`,
Expand Down Expand Up @@ -643,7 +647,11 @@ export async function newDeployment(
? newDeploymentData.newCredentials.url
: "";
try {
const testResult = await api.credentials.test(serverUrl, input);
const testResult = await api.credentials.test(
serverUrl,
!extensionSettings.verifyCertificates(), // insecure = !verifyCertificates
input,
);
if (testResult.status !== 200) {
return Promise.resolve({
message: `Error: Invalid API Key (unable to validate API Key - API Call result: ${testResult.status} - ${testResult.statusText}).`,
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode/src/utils/errorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ErrorCode =
| "invalidTOML"
| "unknownTOMLKey"
| "invalidConfigFile"
| "errorCertificateVerification"
| "deployFailed";

export type axiosErrorWithJson<T = { code: ErrorCode; details: unknown }> =
Expand Down
2 changes: 2 additions & 0 deletions extensions/vscode/src/views/homeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { newCredential } from "src/multiStepInputs/newCredential";
import { PublisherState } from "src/state";
import { throttleWithLastPending } from "src/utils/throttle";
import { showAssociateGUID } from "src/actions/showAssociateGUID";
import { extensionSettings } from "src/extension";

enum HomeViewInitialized {
initialized = "initialized",
Expand Down Expand Up @@ -259,6 +260,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
deploymentName,
credentialName,
configurationName,
!extensionSettings.verifyCertificates(), // insecure = !verifyCertificates
projectDir,
secrets,
);
Expand Down
23 changes: 22 additions & 1 deletion internal/clients/connect/client_connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package connect
// Copyright (C) 2023 by Posit Software, PBC.

import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -100,6 +102,12 @@ func isConnectAuthError(err error) bool {
return errors.As(err, &serr)
}

type certificationValidationFailedDetails struct {
url string
accountName string
certificateError string
}

func (c *ConnectClient) TestAuthentication(log logging.Logger) (*User, error) {
log.Info("Testing authentication", "method", c.account.AuthType.Description(), "url", c.account.URL)
var connectUser UserDTO
Expand All @@ -111,7 +119,20 @@ func (c *ConnectClient) TestAuthentication(log logging.Logger) (*User, error) {
if e, ok := err.(net.Error); ok && e.Timeout() {
log.Debug("Request to Connect timed out")
return nil, ErrTimedOut
} else if isConnectAuthError(err) {
}
if urlError, ok := err.(*url.Error); ok {
if certificateError, ok := urlError.Err.(*tls.CertificateVerificationError); ok {
returnErr := fmt.Errorf("unable to verify TLS certificate for server (%s)", certificateError.Err)
log.Error(returnErr.Error())
details := &certificationValidationFailedDetails{
url: c.account.URL,
accountName: c.account.Name,
certificateError: certificateError.Err.Error(),
}
return nil, types.NewAgentError(types.ErrorCertificateVerification, returnErr, details)
}
}
if isConnectAuthError(err) {
if c.account.ApiKey != "" {
// Key was provided and should have worked
log.Info("Connect API key authentication check failed", "url", c.account.URL)
Expand Down
3 changes: 2 additions & 1 deletion internal/services/api/post_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type PostDeploymentRequestBody struct {
AccountName string `json:"account"`
ConfigName string `json:"config"`
Secrets map[string]string `json:"secrets,omitempty"`
Insecure bool `json:"insecure"`
}

type PostDeploymentsReponse struct {
Expand Down Expand Up @@ -55,7 +56,7 @@ func PostDeploymentHandlerFunc(
InternalError(w, req, log, err)
return
}
newState, err := stateFactory(projectDir, b.AccountName, b.ConfigName, name, "", accountList, b.Secrets)
newState, err := stateFactory(projectDir, b.AccountName, b.ConfigName, name, "", accountList, b.Secrets, b.Insecure)
log.Debug("New account derived state created", "account", b.AccountName, "config", b.ConfigName)
if err != nil {
if errors.Is(err, accounts.ErrAccountNotFound) {
Expand Down
Loading

0 comments on commit 51a563a

Please sign in to comment.