Skip to content

Commit

Permalink
Bug 1678158 - backport oauth2 support to rhcc
Browse files Browse the repository at this point in the history
* fix debug statement
* add version log string
* copy the skip_verify_tls setting to the reg config
* pass client around so we can skip tls on image get
  • Loading branch information
jmrodri committed Mar 19, 2019
1 parent 6a05b72 commit 0c4b40a
Show file tree
Hide file tree
Showing 7 changed files with 460 additions and 43 deletions.
3 changes: 3 additions & 0 deletions pkg/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ func CreateApp() App {
os.Exit(1)
}

// Show version in logs for better debugging
log.Infof("Ansible Service Broker Version: %v", version.Version)

// Initializing clients as soon as we have deps ready.
err = initClients(app.config)
if err != nil {
Expand Down
22 changes: 14 additions & 8 deletions pkg/registries/adapters/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,28 @@ const BundleSpecLabel = "com.redhat.apb.spec"
// Configuration - Adapter configuration. Contains the info that the adapter
// would need to complete its request to the images.
type Configuration struct {
URL *url.URL
User string
Pass string
Org string
Images []string
Namespaces []string
Tag string
URL *url.URL
User string
Pass string
Org string
Images []string
Namespaces []string
Tag string
SkipVerifyTLS bool
}

// Retrieve the spec from a registry manifest request
func imageToSpec(log *logging.Logger, req *http.Request, image string) (*apb.Spec, error) {
return imageToSpecWithClient(http.DefaultClient, log, req, image)
}

// Retrieve the spec from a registry manifest request
func imageToSpecWithClient(client *http.Client, log *logging.Logger, req *http.Request, image string) (*apb.Spec, error) {
log.Debug("Registry::imageToSpec")
spec := &apb.Spec{}
req.Header.Add("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
Expand Down
268 changes: 268 additions & 0 deletions pkg/registries/adapters/oauth/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
//
// Copyright (c) 2018 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package oauth

import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"sync"
"time"

logging "github.com/op/go-logging"
)

// oauth2Response - holds the response data from an oauth2 token request
type oauth2Response struct {
Token string `json:"access_token"`
}

// dockerResponse - holds the response data from a docker token request
type dockerResponse struct {
Token string `json:"token"`
}

// NewClient - creates and returns a *Client ready to use. If skipVerify is
// true, it will skip verification of the remote TLS certificate.
func NewClient(user, pass string, skipVerify bool, url *url.URL, log *logging.Logger) *Client {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: skipVerify},
Proxy: http.ProxyFromEnvironment,
}
if skipVerify == true {
log.Warning("skipping verification of registry TLS certificate per adapter configuration")
}

return &Client{
user: user,
pass: pass,
url: url,
mutex: &sync.Mutex{},
client: &http.Client{Timeout: time.Second * 60, Transport: transport},
log: log,
}
}

// Client - may be used as an HTTP client that automatically authenticates using oauth.
type Client struct {
user string
pass string
token string
mutex *sync.Mutex
client *http.Client
url *url.URL
log *logging.Logger
}

// NewRequest - creates and returns a *http.Request assuming the GET method.
// The base URL configured on the Client gets used with its Path component
// replaced by the path argument. If a token is available, it is added to the
// request automatically. "Accept: application/json" is added to all requests.
// The caller should customize the request as necessary before using it.
func (c *Client) NewRequest(path string) (*http.Request, error) {
req, err := http.NewRequest("GET", c.url.String(), nil)
if err != nil {
return nil, err
}
req.URL.Path = path
if c.token != "" {
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token))
}
req.Header.Add("Accept", "application/json")
return req, nil
}

// Do - passes through to the underlying http.Client instance
func (c *Client) Do(req *http.Request) (*http.Response, error) {
return c.client.Do(req)
}

// GetClient - returns the http.Client
func (c *Client) GetClient() *http.Client {
return c.client
}

// Getv2 - makes a GET request to the registry's /v2/ endpoint. If a 401
// Unauthorized response is received, this method attempts to obtain an oauth
// token and tries again with the new token. If a username and password are
// available, they are used with Basic Auth in the request to the token
// service. This method is goroutine-safe.
func (c *Client) Getv2() error {
// lock to prevent multiple goroutines from retrieving, and especially
// writing, a new token at the same time
c.mutex.Lock()
defer c.mutex.Unlock()
req, err := c.NewRequest("/v2/")
if err != nil {
return err
}
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusUnauthorized:
h := resp.Header.Get("www-authenticate")
c.getToken(h)

// try the new token
tokenReq, err := c.NewRequest("/v2/")
if err != nil {
return err
}
tokenResp, err := c.Do(tokenReq)
if err != nil {
return err
}
defer tokenResp.Body.Close()

if tokenResp.StatusCode != http.StatusOK {
msg := fmt.Sprintf("Token not accepted by /v2/ - %s", tokenResp.Status)
c.log.Warning(msg)
return errors.New(msg)
}
c.log.Debug("GET /v2/ successful with new token")

case http.StatusOK:
if c.token == "" {
c.log.Debug("GET /v2/ successful without token")
} else {
c.log.Debug("GET /v2/ successful with existing token")
}

default:
msg := fmt.Sprintf("Bad response from /v2/ - %s", resp.Status)
c.log.Warning(msg)
return errors.New(msg)
}
return nil
}

// getToken - parses a www-authenticate header and uses the information to
// retrieve an oauth token. The token is stored on the Client and automatically
// added to future requests.
func (c *Client) getToken(wwwauth string) error {
// compute the URL
u, err := parseAuthHeader(wwwauth)
if err != nil {
return err
}

// form the request
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
c.log.Errorf("could not form request: %s", err.Error())
return err
}
if c.pass != "" {
c.log.Debug("adding basic auth to token request")
req.SetBasicAuth(c.user, c.pass)
}

// make the request
resp, err := c.client.Do(req)
if err != nil {
msg := fmt.Sprintf("error obtaining token: %s", err.Error())
c.log.Warning(msg)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg := fmt.Sprintf("token service responded: %s", resp.Status)
c.log.Warning(msg)
return errors.New(msg)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
c.log.Warningf("failed to read token body: %s", err.Error())
return err
}

c.token, err = parseAuthToken(body)
if err != nil {
return err
}

c.log.Debugf("new token: %s", c.token)
return nil
}

// parseAuthHeader - parses the text from a www-authenticate header and uses it
// to construct a url.URL that can be used to retrieve a token.
func parseAuthHeader(value string) (*url.URL, error) {
rrealm, err := regexp.Compile("realm=\"([^\"]+)\"")
if err != nil {
return nil, err
}
rservice, err := regexp.Compile("service=\"([^\"]+)\"")
if err != nil {
return nil, err
}

rmatch := rrealm.FindStringSubmatch(value)
if len(rmatch) != 2 {
msg := fmt.Sprintf("Could not parse www-authenticate header: %s", value)
return nil, errors.New(msg)
}
realm := rmatch[1]

u, err := url.Parse(realm)
if err != nil {
msg := fmt.Sprintf("realm is not a valid URL: %s", realm)
return nil, errors.New(msg)
}

smatch := rservice.FindStringSubmatch(value)
if len(smatch) == 2 {
service := smatch[1]
q := u.Query()
q.Set("service", service)
u.RawQuery = q.Encode()
}

return u, nil
}

// parseAuthToken - parses a token from a http response body in json format.
// The token can be located in the fields "access_token" or "token" of the json
// response. This method returns the value of one of those fields (favouring
// "access_token" if both are set) or an empty string if non of the fields is set.
func parseAuthToken(body []byte) (string, error) {
oauth2Resp := oauth2Response{}
err := json.Unmarshal(body, &oauth2Resp)
if err != nil {
return "", err
}

if oauth2Resp.Token != "" {
return oauth2Resp.Token, nil
}

dockerResp := dockerResponse{}
err = json.Unmarshal(body, &dockerResp)
if err != nil {
return "", err
}
return dockerResp.Token, nil
}

0 comments on commit 0c4b40a

Please sign in to comment.