Skip to content

Commit

Permalink
Add independent optional uaa-target flag
Browse files Browse the repository at this point in the history
- Adds the ability to override the default Opsman UAA endpoint to aid in development.
- Centralize target URL parsing and validation with tests.
- Add error message returned from UAA when auth fails to aid in troubleshooting - like when an account is locked out.
  • Loading branch information
sneal authored and wayneadams committed Apr 11, 2024
1 parent af5c27a commit 4a43857
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 154 deletions.
13 changes: 13 additions & 0 deletions cmd/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd_test

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"testing"
)

func TestCommands(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "cmd")
}
7 changes: 0 additions & 7 deletions cmd/loadConfigFile_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cmd

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
Expand All @@ -22,8 +20,3 @@ var _ = Describe("parseOptions", func() {

})
})

func TestCmds(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Cmds")
}
68 changes: 61 additions & 7 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
Expand All @@ -29,16 +30,17 @@ type httpClient interface {
}

type options struct {
CACert string `yaml:"ca-cert" long:"ca-cert" env:"OM_CA_CERT" description:"OpsManager CA certificate path or value"`
CACert string `yaml:"ca-cert" long:"ca-cert" env:"OM_CA_CERT" description:"OpsManager CA certificate path or value"`
ClientID string `yaml:"client-id" short:"c" long:"client-id" env:"OM_CLIENT_ID" description:"Client ID for the Ops Manager VM (not required for unauthenticated commands)"`
ClientSecret string `yaml:"client-secret" short:"s" long:"client-secret" env:"OM_CLIENT_SECRET" description:"Client Secret for the Ops Manager VM (not required for unauthenticated commands)"`
ConnectTimeout int `yaml:"connect-timeout" short:"o" long:"connect-timeout" env:"OM_CONNECT_TIMEOUT" default:"10" description:"timeout in seconds to make TCP connections"`
DecryptionPassphrase string `yaml:"decryption-passphrase" short:"d" long:"decryption-passphrase" env:"OM_DECRYPTION_PASSPHRASE" description:"Passphrase to decrypt the installation if the Ops Manager VM has been rebooted (optional for most commands)"`
Env string ` short:"e" long:"env" description:"env file with login credentials"`
DecryptionPassphrase string `yaml:"decryption-passphrase" short:"d" long:"decryption-passphrase" env:"OM_DECRYPTION_PASSPHRASE" description:"Passphrase to decrypt the installation if the Ops Manager VM has been rebooted (optional for most commands)"`
Env string ` short:"e" long:"env" description:"env file with login credentials"`
Password string `yaml:"password" short:"p" long:"password" env:"OM_PASSWORD" description:"admin password for the Ops Manager VM (not required for unauthenticated commands)"`
RequestTimeout int `yaml:"request-timeout" short:"r" long:"request-timeout" env:"OM_REQUEST_TIMEOUT" default:"1800" description:"timeout in seconds for HTTP requests to Ops Manager"`
SkipSSLValidation bool `yaml:"skip-ssl-validation" short:"k" long:"skip-ssl-validation" env:"OM_SKIP_SSL_VALIDATION" description:"skip ssl certificate validation during http requests"`
Target string `yaml:"target" short:"t" long:"target" env:"OM_TARGET" description:"location of the Ops Manager VM"`
UAATarget string `yaml:"uaa-target" long:"uaa-target" env:"OM_UAA_TARGET" description:"location of the Ops Manager VM"`
Trace bool `yaml:"trace" long:"trace" env:"OM_TRACE" description:"prints HTTP requests and response payloads"`
Username string `yaml:"username" short:"u" long:"username" env:"OM_USERNAME" description:"admin username for the Ops Manager VM (not required for unauthenticated commands)"`
VarsEnv string ` long:"vars-env" env:"OM_VARS_ENV" description:"load vars from environment variables by specifying a prefix (e.g.: 'MY' to load MY_var=value)"`
Expand Down Expand Up @@ -73,13 +75,18 @@ func Main(sout io.Writer, serr io.Writer, version string, applySleepDurationStri
requestTimeout := time.Duration(global.RequestTimeout) * time.Second
connectTimeout := time.Duration(global.ConnectTimeout) * time.Second

opsmanURL, uaaURL, err := parseTargetURLs(global.Target, global.UAATarget)
if err != nil {
return err
}

var unauthenticatedClient, authedClient, unauthenticatedProgressClient, authedProgressClient httpClient
unauthenticatedClient, err = network.NewUnauthenticatedClient(global.Target, global.SkipSSLValidation, global.CACert, connectTimeout, requestTimeout)
unauthenticatedClient, err = network.NewUnauthenticatedClient(opsmanURL, global.SkipSSLValidation, global.CACert, connectTimeout, requestTimeout)
if err != nil {
return err
}

authedClient, err = network.NewOAuthClient(global.Target, global.Username, global.Password, global.ClientID, global.ClientSecret, global.SkipSSLValidation, global.CACert, connectTimeout, requestTimeout)
authedClient, err = network.NewOAuthClient(uaaURL, opsmanURL, global.Username, global.Password, global.ClientID, global.ClientSecret, global.SkipSSLValidation, global.CACert, connectTimeout, requestTimeout)

if err != nil {
return err
Expand Down Expand Up @@ -189,7 +196,7 @@ func Main(sout io.Writer, serr io.Writer, version string, applySleepDurationStri
"bosh-env",
"prints environment variables for BOSH and Credhub",
"This prints environment variables to target the BOSH director and Credhub. You can invoke it directly to see its output, or use it directly with an evaluate-type command:\nOn posix system: eval \"$(om bosh-env)\"\nOn powershell: iex $(om bosh-env | Out-String)",
commands.NewBoshEnvironment(api, stdout, global.Target, envRendererFactory),
commands.NewBoshEnvironment(api, stdout, opsmanURL.String(), envRendererFactory),
)
if err != nil {
return err
Expand Down Expand Up @@ -261,7 +268,7 @@ func Main(sout io.Writer, serr io.Writer, version string, applySleepDurationStri
"configure-product",
"configures a staged product",
"This authenticated command configures a staged product",
commands.NewConfigureProduct(os.Environ, api, global.Target, stdout),
commands.NewConfigureProduct(os.Environ, api, opsmanURL.String(), stdout),
)
if err != nil {
return err
Expand Down Expand Up @@ -717,6 +724,9 @@ func setEnvFileProperties(global *options) error {
if global.Target == "" {
global.Target = opts.Target
}
if global.UAATarget == "" {
global.UAATarget = opts.UAATarget
}
if !global.Trace {
global.Trace = opts.Trace
}
Expand Down Expand Up @@ -777,3 +787,47 @@ func checkForVars(opts *options) error {

return nil
}

func parseTargetURLs(opsmanTarget, uaaTarget string) (*url.URL, *url.URL, error) {
parseURL := func(u string) (*url.URL, error) {
// default the target protocol to https if none specified
if !strings.Contains(u, "://") {
u = "https://" + u
}

targetURL, err := url.Parse(u)
if err != nil {
return nil, err
}

// at a minimum ensure we have a host with http(s) protocol
if targetURL.Scheme != "https" && targetURL.Scheme != "http" {
return nil, fmt.Errorf("error parsing target, expected http(s) protocol but got: %s", targetURL.Scheme)
}
if targetURL.Host == "" {
return nil, errors.New("target flag is required, run `om help` for more info")
}

return targetURL, nil
}

opsmanURL, err := parseURL(opsmanTarget)
if err != nil {
return nil, nil, fmt.Errorf("could not parse Opsman target URL: %w", err)
}

var uaaURL *url.URL
if uaaTarget != "" {
uaaURL, err = parseURL(uaaTarget)
if err != nil {
return nil, nil, fmt.Errorf("could not parse UAA target URL: %w", err)
}
} else {
// default to opsman URL with /uaa path (shallow copy)
t := *opsmanURL
t.Path = "/uaa"
uaaURL = &t
}

return opsmanURL, uaaURL, nil
}
74 changes: 74 additions & 0 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("main", func() {
It("Should parse Opsman target URL with https protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("https://opsman.example.com", "")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("https://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("https://opsman.example.com/uaa"))
})

It("Should parse Opsman target URL with http protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("http://opsman.example.com", "")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("http://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("http://opsman.example.com/uaa"))
})

It("Should parse Opsman target URL without protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("opsman.example.com", "")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("https://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("https://opsman.example.com/uaa"))
})

It("Should parse Opsman and UAA target URL with https protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("https://opsman.example.com", "https://uaa.example.com")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("https://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("https://uaa.example.com"))
})

It("Should parse Opsman and UAA target URL with http protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("http://opsman.example.com", "http://uaa.example.com")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("http://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("http://uaa.example.com"))
})

It("Should parse Opsman and UAA target URL without protocol", func() {
opsmanURL, uaaURL, err := parseTargetURLs("opsman.example.com", "uaa.example.com")
Expect(err).ToNot(HaveOccurred())
Expect(opsmanURL.String()).To(Equal("https://opsman.example.com"))
Expect(uaaURL.String()).To(Equal("https://uaa.example.com"))
})

It("Should return flag required error when Opsman target URL when empty", func() {
_, _, err := parseTargetURLs("", "")
Expect(err).To(HaveOccurred())
Expect(err).To(MatchError("could not parse Opsman target URL: target flag is required, run `om help` for more info"))
})

It("Should not parse Opsman target URL with incorrect protocol", func() {
_, _, err := parseTargetURLs("smb://opsman.example.com", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("error parsing target, expected http(s) protocol but got: smb"))
})

It("Should not parse Opsman target URL with bad URL", func() {
_, _, err := parseTargetURLs("a bad\\url", "")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not parse Opsman target URL"))
})

It("Should not parse UAA target URL with bad URL", func() {
_, _, err := parseTargetURLs("opsman.example.com", "a bad\\url")
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not parse UAA target URL"))
})
})
46 changes: 23 additions & 23 deletions network/oauth_client.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package network

import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/cloudfoundry-community/go-uaa"
Expand All @@ -17,63 +17,59 @@ type OAuthClient struct {
clientSecret string
insecureSkipVerify bool
password string
target string
target *url.URL
uaaTarget *url.URL
token *oauth2.Token
username string
connectTimeout time.Duration
requestTimeout time.Duration
}

func NewOAuthClient(
target, username, password string,
uaaTarget, opsmanTarget *url.URL,
username, password string,
clientID, clientSecret string,
insecureSkipVerify bool,
caCert string,
connectTimeout time.Duration,
requestTimeout time.Duration,
) (*OAuthClient, error) {
if uaaTarget == nil {
return nil, errors.New("expected a non-nil UAA target")
}
if opsmanTarget == nil {
return nil, errors.New("expected a non-nil target")
}

return &OAuthClient{
caCert: caCert,
clientID: clientID,
clientSecret: clientSecret,
insecureSkipVerify: insecureSkipVerify,
password: password,
target: target,
uaaTarget: uaaTarget,
target: opsmanTarget,
username: username,
connectTimeout: connectTimeout,
requestTimeout: requestTimeout,
}, nil
}

func (oc *OAuthClient) Do(request *http.Request) (*http.Response, error) {
token := oc.token
target := oc.target

if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
target = "https://" + target
}

targetURL, err := url.Parse(target)
if err != nil {
return nil, fmt.Errorf("could not parse target url: %s", err)
}

targetURL.Path = "/uaa"

request.URL.Scheme = targetURL.Scheme
request.URL.Host = targetURL.Host
request.URL.Scheme = oc.target.Scheme
request.URL.Host = oc.target.Host

client, err := newHTTPClient(
oc.insecureSkipVerify,
oc.caCert,
oc.requestTimeout,
oc.connectTimeout,
)

if err != nil {
return nil, err
}

token := oc.token
if token != nil && token.Valid() {
request.Header.Set(
"Authorization",
Expand Down Expand Up @@ -106,7 +102,7 @@ func (oc *OAuthClient) Do(request *http.Request) (*http.Response, error) {
}

api, err := uaa.New(
targetURL.String(),
oc.uaaTarget.String(),
authOption,
options...,
)
Expand All @@ -122,7 +118,11 @@ func (oc *OAuthClient) Do(request *http.Request) (*http.Response, error) {
}

if err != nil {
return nil, fmt.Errorf("token could not be retrieved from target url: %w", err)
if oauthErr, ok := err.(uaa.RequestError); ok {
return nil, fmt.Errorf("token could not be retrieved from target: %w: %s",
err, string(oauthErr.ErrorResponse))
}
return nil, fmt.Errorf("token could not be retrieved from target : %w", err)
}

request.Header.Set(
Expand Down
Loading

0 comments on commit 4a43857

Please sign in to comment.