Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug 1678158 - backport oauth2 support to rhcc #1194

Merged
merged 1 commit into from
Mar 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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
}
Loading