Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
9159: Add --insecure option to `pulumi login` r=justinvp a=Frassle

<!--- 
Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation.
-->

# Description

<!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. -->

Add --insecure option to `pulumi login` which disables https certificate checks.

Fixes #9120

## Checklist

<!--- Please provide details if the checkbox below is to be left unchecked. -->
- [ ] I have added tests that prove my fix is effective or that my feature works
<!--- 
User-facing changes require a CHANGELOG entry.
-->
- [ ] I have updated the [CHANGELOG-PENDING](https://github.com/pulumi/pulumi/blob/master/CHANGELOG_PENDING.md) file with my change
<!--
If the change(s) in this PR is a modification of an existing call to the Pulumi Service,
then the service should honor older versions of the CLI where this change would not exist.
You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Service API version
  <!-- `@Pulumi` employees: If yes, you must submit corresponding changes in the service repo. -->


12001: Support clones from Azure DevOps r=RobbieMcKinstry a=squaremo

In general, go-git can't clone from Azure DevOps, because the latter requires the capabilities multi_ack and multi_ack_detailed, which aren't implemented. However, there's now a workaround, which boils down to this: pretend, for the initial clone, that those capabilities _are_ supported, and expect them not to be used.

(See go-git/go-git#613 for more on this workaround.)

I tried this with a personal Azure DevOps account; an automated test would need either a reliably long-lived Azure DevOps repo, or a test server that can mimic Azure DevOps' particular capabilities. I'm open to suggestions!

12025: [sdks/go] Delegate alias computation to the engine r=abhinav a=Zaid-Ajaj

Fixes #11066
Addresses #11697 

Credit to `@abhinav` for making aliases unit-testable by intercepting `RegisterResource` calls. 

> I did change the test slightly so that it either checks for `AliasURNs: []string` or `Aliases: []*pulumirpc.Alias` because I've made it such that one of them is `nil` depending on `supportsAliasSpecs`

## Checklist

<!--- Please provide details if the checkbox below is to be left unchecked. -->
- [x] I have added tests that prove my fix is effective or that my feature works
<!--- 
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the Pulumi Service,
then the service should honor older versions of the CLI where this change would not exist.
You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Service API version
  <!-- `@Pulumi` employees: If yes, you must submit corresponding changes in the service repo. -->


Co-authored-by: Fraser Waters <fraser@pulumi.com>
Co-authored-by: Michael Bridgen <mbridgen@pulumi.com>
Co-authored-by: Zaid Ajaj <zaid.naom@gmail.com>
  • Loading branch information
4 people committed Feb 4, 2023
4 parents ad58313 + 7d53b47 + 7843de3 + bd17206 commit 63ea380
Show file tree
Hide file tree
Showing 26 changed files with 807 additions and 302 deletions.
@@ -0,0 +1,4 @@
changes:
- type: chore
scope: sdk/go
description: Delegate alias computation to the engine
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: auto
description: Adds support for cloning from Azure DevOps
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: cli
description: Add `--insecure` flag to `pulumi login` which disables https certificate checks
58 changes: 32 additions & 26 deletions pkg/backend/httpstate/backend.go
Expand Up @@ -129,7 +129,7 @@ type cloudBackend struct {
var _ backend.SpecificDeploymentExporter = &cloudBackend{}

// New creates a new Pulumi backend for the given cloud API URL and token.
func New(d diag.Sink, cloudURL string) (Backend, error) {
func New(d diag.Sink, cloudURL string, insecure bool) (Backend, error) {
cloudURL = ValueOrDefaultURL(cloudURL)
account, err := workspace.GetAccount(cloudURL)
if err != nil {
Expand All @@ -143,7 +143,7 @@ func New(d diag.Sink, cloudURL string) (Backend, error) {
currentProject = nil
}

client := client.NewClient(cloudURL, apiToken, d)
client := client.NewClient(cloudURL, apiToken, insecure, d)
capabilities := detectCapabilities(d, client)

return &cloudBackend{
Expand All @@ -156,7 +156,9 @@ func New(d diag.Sink, cloudURL string) (Backend, error) {
}

// loginWithBrowser uses a web-browser to log into the cloud and returns the cloud backend for it.
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string,
insecure bool, opts display.Options) (Backend, error) {

// Locally, we generate a nonce and spin up a web server listening on a random port on localhost. We then open a
// browser to a special endpoint on the Pulumi.com console, passing the generated nonce as well as the port of the
// webserver we launched. This endpoint does the OAuth flow and when it completes, redirects to localhost passing
Expand Down Expand Up @@ -223,7 +225,7 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts di

accessToken := <-c

username, organizations, err := client.NewClient(cloudURL, accessToken, d).GetPulumiAccountDetails(ctx)
username, organizations, err := client.NewClient(cloudURL, accessToken, insecure, d).GetPulumiAccountDetails(ctx)
if err != nil {
return nil, err
}
Expand All @@ -234,6 +236,7 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts di
Username: username,
Organizations: organizations,
LastValidatedAt: time.Now(),
Insecure: insecure,
}
if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
return nil, err
Expand All @@ -242,16 +245,16 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts di
// Welcome the user since this was an interactive login.
WelcomeUser(opts)

return New(d, cloudURL)
return New(d, cloudURL, insecure)
}

// LoginManager provides a slim wrapper around functions related to backend logins.
type LoginManager interface {
// Current returns the current cloud backend if one is already logged in.
Current(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error)
Current(ctx context.Context, d diag.Sink, cloudURL string, insecure bool) (Backend, error)

// Login logs into the target cloud URL and returns the cloud backend for it.
Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error)
Login(ctx context.Context, d diag.Sink, cloudURL string, insecure bool, opts display.Options) (Backend, error)
}

// NewLoginManager returns a LoginManager for handling backend logins.
Expand All @@ -268,7 +271,9 @@ var newLoginManager = func() LoginManager {
type defaultLoginManager struct{}

// Current returns the current cloud backend if one is already logged in.
func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL string) (Backend, error) {
func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL string,
insecure bool) (Backend, error) {

cloudURL = ValueOrDefaultURL(cloudURL)

// If we have a saved access token, and it is valid, use it.
Expand All @@ -277,7 +282,7 @@ func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL
// If the account was last verified less than an hour ago, assume the token is valid.
valid, username, organizations := true, existingAccount.Username, existingAccount.Organizations
if username == "" || existingAccount.LastValidatedAt.Add(1*time.Hour).Before(time.Now()) {
valid, username, organizations, err = IsValidAccessToken(ctx, cloudURL, existingAccount.AccessToken)
valid, username, organizations, err = IsValidAccessToken(ctx, cloudURL, insecure, existingAccount.AccessToken)
if err != nil {
return nil, err
}
Expand All @@ -288,11 +293,12 @@ func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL
// Save the token. While it hasn't changed this will update the current cloud we are logged into, as well.
existingAccount.Username = username
existingAccount.Organizations = organizations
existingAccount.Insecure = insecure
if err = workspace.StoreAccount(cloudURL, existingAccount, true); err != nil {
return nil, err
}

return New(d, cloudURL)
return New(d, cloudURL, insecure)
}
}

Expand All @@ -310,7 +316,7 @@ func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL
contract.IgnoreError(err)

// Try and use the credentials to see if they are valid.
valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, accessToken)
valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, insecure, accessToken)
if err != nil {
return nil, err
} else if !valid {
Expand All @@ -323,19 +329,20 @@ func (m defaultLoginManager) Current(ctx context.Context, d diag.Sink, cloudURL
Username: username,
Organizations: organizations,
LastValidatedAt: time.Now(),
Insecure: insecure,
}
if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
return nil, err
}

return New(d, cloudURL)
return New(d, cloudURL, insecure)
}

// Login logs into the target cloud URL and returns the cloud backend for it.
func (m defaultLoginManager) Login(
ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
ctx context.Context, d diag.Sink, cloudURL string, insecure bool, opts display.Options) (Backend, error) {

current, err := m.Current(ctx, d, cloudURL)
current, err := m.Current(ctx, d, cloudURL, insecure)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -401,15 +408,15 @@ func (m defaultLoginManager) Login(
}

if accessToken == "" {
return loginWithBrowser(ctx, d, cloudURL, opts)
return loginWithBrowser(ctx, d, cloudURL, insecure, opts)
}

// Welcome the user since this was an interactive login.
WelcomeUser(opts)
}

// Try and use the credentials to see if they are valid.
valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, accessToken)
valid, username, organizations, err := IsValidAccessToken(ctx, cloudURL, insecure, accessToken)
if err != nil {
return nil, err
} else if !valid {
Expand All @@ -422,12 +429,13 @@ func (m defaultLoginManager) Login(
Username: username,
Organizations: organizations,
LastValidatedAt: time.Now(),
Insecure: insecure,
}
if err = workspace.StoreAccount(cloudURL, account, true); err != nil {
return nil, err
}

return New(d, cloudURL)
return New(d, cloudURL, insecure)
}

// WelcomeUser prints a Welcome to Pulumi message.
Expand Down Expand Up @@ -535,17 +543,11 @@ func (b *cloudBackend) GetPolicyPack(ctx context.Context, policyPack string,
return nil, err
}

account, err := workspace.GetAccount(b.CloudURL())
if err != nil {
return nil, err
}
apiToken := account.AccessToken

return &cloudPolicyPack{
ref: newCloudBackendPolicyPackReference(b.CloudConsoleURL(),
policyPackRef.OrgName(), policyPackRef.Name()),
b: b,
cl: client.NewClient(b.CloudURL(), apiToken, d)}, nil
cl: b.client}, nil
}

func (b *cloudBackend) ListPolicyGroups(ctx context.Context, orgName string, inContToken backend.ContinuationToken) (
Expand Down Expand Up @@ -1583,11 +1585,15 @@ func (b *cloudBackend) tryNextUpdate(ctx context.Context, update client.UpdateId

// IsValidAccessToken tries to use the provided Pulumi access token and returns if it is accepted
// or not. Returns error on any unexpected error.
func IsValidAccessToken(ctx context.Context, cloudURL, accessToken string) (bool, string, []string, error) {
func IsValidAccessToken(ctx context.Context, cloudURL string,
insecure bool, accessToken string) (bool, string, []string, error) {

// Make a request to get the authenticated user. If it returns a successful response,
// we know the access token is legit. We also parse the response as JSON and confirm
// it has a githubLogin field that is non-empty (like the Pulumi Service would return).
username, organizations, err := client.NewClient(cloudURL, accessToken, cmdutil.Diag()).GetPulumiAccountDetails(ctx)
username, organizations, err := client.NewClient(cloudURL, accessToken,
insecure, cmdutil.Diag()).GetPulumiAccountDetails(ctx)

if err != nil {
if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == 401 {
return false, "", nil, nil
Expand Down
59 changes: 40 additions & 19 deletions pkg/backend/httpstate/client/client.go
Expand Up @@ -16,6 +16,7 @@ package client

import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -43,35 +44,55 @@ import (

// Client provides a slim wrapper around the Pulumi HTTP/REST API.
type Client struct {
apiURL string
apiToken apiAccessToken
apiUser string
apiOrgs []string
diag diag.Sink
client restClient
apiURL string
apiToken apiAccessToken
apiUser string
apiOrgs []string
diag diag.Sink
insecure bool
restClient restClient
httpClient *http.Client

// If true, do not probe the backend with GET /api/capabilities and assume no capabilities.
DisableCapabilityProbing bool
}

// newClient creates a new Pulumi API client with the given URL and API token. It is a variable instead of a regular
// function so it can be set to a different implementation at runtime, if necessary.
var newClient = func(apiURL, apiToken string, d diag.Sink) *Client {
var newClient = func(apiURL, apiToken string, insecure bool, d diag.Sink) *Client {

var httpClient *http.Client
if insecure {
tr := &http.Transport{
//nolint:gosec // The user has explicitly opted into setting this
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient = &http.Client{Transport: tr}
} else {
httpClient = http.DefaultClient
}

return &Client{
apiURL: apiURL,
apiToken: apiAccessToken(apiToken),
diag: d,
client: &defaultRESTClient{
apiURL: apiURL,
apiToken: apiAccessToken(apiToken),
diag: d,
httpClient: httpClient,
restClient: &defaultRESTClient{
client: &defaultHTTPClient{
client: http.DefaultClient,
client: httpClient,
},
},
}
}

// Returns true if this client is insecure (i.e. has TLS disabled).
func (pc *Client) Insecure() bool {
return pc.insecure
}

// NewClient creates a new Pulumi API client with the given URL and API token.
func NewClient(apiURL, apiToken string, d diag.Sink) *Client {
return newClient(apiURL, apiToken, d)
func NewClient(apiURL, apiToken string, insecure bool, d diag.Sink) *Client {
return newClient(apiURL, apiToken, insecure, d)
}

// URL returns the URL of the API endpoint this client interacts with
Expand All @@ -82,15 +103,15 @@ func (pc *Client) URL() string {
// restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
// object. If a response object is provided, the server's response is deserialized into that object.
func (pc *Client) restCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{}) error {
return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken,
return pc.restClient.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken,
httpCallOptions{})
}

// restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
// object. If a response object is provided, the server's response is deserialized into that object.
func (pc *Client) restCallWithOptions(ctx context.Context, method, path string, queryObj, reqObj,
respObj interface{}, opts httpCallOptions) error {
return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken, opts)
return pc.restClient.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken, opts)
}

// updateRESTCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
Expand All @@ -99,7 +120,7 @@ func (pc *Client) restCallWithOptions(ctx context.Context, method, path string,
func (pc *Client) updateRESTCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{},
token updateAccessToken, httpOptions httpCallOptions) error {

return pc.client.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, token, httpOptions)
return pc.restClient.Call(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, token, httpOptions)
}

// getProjectPath returns the API path for the given owner and the given project name joined with path separators
Expand Down Expand Up @@ -703,7 +724,7 @@ func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
putReq.Header.Add(k, v)
}

_, err = http.DefaultClient.Do(putReq)
_, err = pc.httpClient.Do(putReq)
if err != nil {
return "", fmt.Errorf("Failed to upload compressed PolicyPack: %w", err)
}
Expand Down Expand Up @@ -856,7 +877,7 @@ func (pc *Client) DownloadPolicyPack(ctx context.Context, url string) (io.ReadCl
return nil, fmt.Errorf("Failed to download compressed PolicyPack: %w", err)
}

resp, err := http.DefaultClient.Do(getS3Req)
resp, err := pc.httpClient.Do(getS3Req)
if err != nil {
return nil, fmt.Errorf("Failed to download compressed PolicyPack: %w", err)
}
Expand Down
15 changes: 9 additions & 6 deletions pkg/backend/httpstate/client/client_test.go
Expand Up @@ -52,14 +52,17 @@ func newMockServerRequestProcessor(statusCode int, processor func(req *http.Requ
}

func newMockClient(server *httptest.Server) *Client {
httpClient := http.DefaultClient

return &Client{
apiURL: server.URL,
apiToken: "",
apiUser: "",
diag: nil,
client: &defaultRESTClient{
apiURL: server.URL,
apiToken: "",
apiUser: "",
diag: nil,
httpClient: httpClient,
restClient: &defaultRESTClient{
client: &defaultHTTPClient{
client: http.DefaultClient,
client: httpClient,
},
},
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/backend/httpstate/snapshot_test.go
Expand Up @@ -163,7 +163,7 @@ func TestCloudSnapshotPersisterUseOfDiffProtocol(t *testing.T) {

initPersister := func() *cloudSnapshotPersister {
server := newMockServer()
backendGeneric, err := New(nil, server.URL)
backendGeneric, err := New(nil, server.URL, false)
assert.NoError(t, err)
backend := backendGeneric.(*cloudBackend)
persister := backend.newSnapshotPersister(ctx, client.UpdateIdentifier{
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/pulumi/login.go
Expand Up @@ -35,6 +35,7 @@ func newLoginCmd() *cobra.Command {
var cloudURL string
var defaultOrg string
var localMode bool
var insecure bool

cmd := &cobra.Command{
Use: "login [<url>]",
Expand Down Expand Up @@ -141,7 +142,7 @@ func newLoginCmd() *cobra.Command {
return fmt.Errorf("unable to set default org for this type of backend")
}
} else {
be, err = httpstate.NewLoginManager().Login(ctx, cmdutil.Diag(), cloudURL, displayOptions)
be, err = httpstate.NewLoginManager().Login(ctx, cmdutil.Diag(), cloudURL, insecure, displayOptions)
// if the user has specified a default org to associate with the backend
if defaultOrg != "" {
cloudURL, err := workspace.GetCurrentCloudURL()
Expand Down Expand Up @@ -171,6 +172,7 @@ func newLoginCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&defaultOrg, "default-org", "", "A default org to associate with the login. "+
"Please note, currently, only the managed and self-hosted backends support organizations")
cmd.PersistentFlags().BoolVarP(&localMode, "local", "l", false, "Use Pulumi in local-only mode")
cmd.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow insecure server connections when using SSL")

return cmd
}
Expand Down

0 comments on commit 63ea380

Please sign in to comment.