svchost/disco: Allow services that act as OAuth clients
The OAuth specification requires several distinct arguments to be provided to configure a client, rather than just a URL. To accommodate this, we'll add a new method to the service discovery API to retrieve OAuth client information in a Terraform-specific form. (The OAuth specification itself considers this out of scope, because most OAuth clients are configured by just hard-coding these settings into them for a particular remote service.)
This commit is contained in:
parent
1140ca9cb5
commit
5590efcd33
|
@ -111,27 +111,150 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
|||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
u, err := h.parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse service URL: %v", err)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ServiceOAuthClient returns the OAuth client configuration associated with the
|
||||
// given service identifier, which should be of the form "servicename.vN".
|
||||
//
|
||||
// This is an alternative to ServiceURL for unusual services that require
|
||||
// a full OAuth2 client definition rather than just a URL. Use this only
|
||||
// for services whose specification calls for this sort of definition.
|
||||
func (h *Host) ServiceOAuthClient(id string) (*OAuthClient, error) {
|
||||
svc, ver, err := parseServiceID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No services supported for an empty Host.
|
||||
if h == nil || h.services == nil {
|
||||
return nil, &ErrServiceNotProvided{service: svc}
|
||||
}
|
||||
|
||||
if _, ok := h.services[id]; !ok {
|
||||
// See if we have a matching service as that would indicate
|
||||
// the service is supported, but not the requested version.
|
||||
for serviceID := range h.services {
|
||||
if strings.HasPrefix(serviceID, svc+".") {
|
||||
return nil, &ErrVersionNotSupported{
|
||||
hostname: h.hostname,
|
||||
service: svc,
|
||||
version: ver.Original(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No discovered services match the requested service.
|
||||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
|
||||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
switch v := h.services[id].(type) {
|
||||
case map[string]interface{}:
|
||||
raw = v // Great!
|
||||
case []map[string]interface{}:
|
||||
// An absolutely infuriating legacy HCL ambiguity.
|
||||
raw = v[0]
|
||||
default:
|
||||
// Debug message because raw Go types don't belong in our UI.
|
||||
log.Printf("[DEBUG] The definition for %s has Go type %T", id, h.services[id])
|
||||
return nil, fmt.Errorf("Service %s must be declared with an object value in the service discovery document", id)
|
||||
}
|
||||
|
||||
ret := &OAuthClient{}
|
||||
if clientIDStr, ok := raw["client"].(string); ok {
|
||||
ret.ID = clientIDStr
|
||||
} else {
|
||||
return nil, fmt.Errorf("Service %s definition is missing required property \"client\"", id)
|
||||
}
|
||||
if urlStr, ok := raw["authz"].(string); ok {
|
||||
u, err := h.parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse authorization URL: %v", err)
|
||||
}
|
||||
ret.AuthorizationURL = u
|
||||
} else {
|
||||
return nil, fmt.Errorf("Service %s definition is missing required property \"authz\"", id)
|
||||
}
|
||||
if urlStr, ok := raw["token"].(string); ok {
|
||||
u, err := h.parseURL(urlStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse token URL: %v", err)
|
||||
}
|
||||
ret.TokenURL = u
|
||||
} else {
|
||||
return nil, fmt.Errorf("Service %s definition is missing required property \"token\"", id)
|
||||
}
|
||||
if portsRaw, ok := raw["ports"].([]interface{}); ok {
|
||||
if len(portsRaw) != 2 {
|
||||
return nil, fmt.Errorf("Invalid \"ports\" definition for service %s: must be a two-element array", id)
|
||||
}
|
||||
invalidPortsErr := fmt.Errorf("Invalid \"ports\" definition for service %s: both ports must be whole numbers between 1024 and 65535", id)
|
||||
ports := make([]uint16, 2)
|
||||
for i := range ports {
|
||||
switch v := portsRaw[i].(type) {
|
||||
case float64:
|
||||
// JSON unmarshaling always produces float64. HCL 2 might, if
|
||||
// an invalid fractional number were given.
|
||||
if float64(uint16(v)) != v || v < 1024 {
|
||||
return nil, invalidPortsErr
|
||||
}
|
||||
ports[i] = uint16(v)
|
||||
case int:
|
||||
// Legacy HCL produces int. HCL 2 will too, if the given number
|
||||
// is a whole number.
|
||||
if v < 1024 || v > 65535 {
|
||||
return nil, invalidPortsErr
|
||||
}
|
||||
ports[i] = uint16(v)
|
||||
default:
|
||||
// Debug message because raw Go types don't belong in our UI.
|
||||
log.Printf("[DEBUG] Port value %d has Go type %T", i, portsRaw[i])
|
||||
return nil, invalidPortsErr
|
||||
}
|
||||
}
|
||||
if ports[1] < ports[0] {
|
||||
return nil, fmt.Errorf("Invalid \"ports\" definition for service %s: minimum port cannot be greater than maximum port", id)
|
||||
}
|
||||
ret.MinPort = ports[0]
|
||||
ret.MaxPort = ports[1]
|
||||
} else {
|
||||
// Default is to accept any port in the range, for a client that is
|
||||
// able to call back to any localhost port.
|
||||
ret.MinPort = 1024
|
||||
ret.MaxPort = 65535
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (h *Host) parseURL(urlStr string) (*url.URL, error) {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make relative URLs absolute using our discovery URL.
|
||||
if !u.IsAbs() {
|
||||
u = h.discoURL.ResolveReference(u)
|
||||
}
|
||||
|
||||
if u.Scheme != "https" && u.Scheme != "http" {
|
||||
return nil, fmt.Errorf("Service URL is using an unsupported scheme: %s", u.Scheme)
|
||||
return nil, fmt.Errorf("unsupported scheme %s", u.Scheme)
|
||||
}
|
||||
if u.User != nil {
|
||||
return nil, fmt.Errorf("Embedded username/password information is not permitted")
|
||||
return nil, fmt.Errorf("embedded username/password information is not permitted")
|
||||
}
|
||||
|
||||
// Fragment part is irrelevant, since we're not a browser.
|
||||
u.Fragment = ""
|
||||
|
||||
return h.discoURL.ResolveReference(u), nil
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// VersionConstraints returns the contraints for a given service identifier
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package disco
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OAuthClient represents an OAuth client configuration, which is used for
|
||||
// unusual services that require an entire OAuth client configuration as part
|
||||
// of their service discovery, rather than just a URL.
|
||||
type OAuthClient struct {
|
||||
// ID is the identifier for the client, to be used as "client_id" in
|
||||
// OAuth requests.
|
||||
ID string
|
||||
|
||||
// Authorization URL is the URL of the authorization endpoint that must
|
||||
// be used for this OAuth client, as defined in the OAuth2 specifications.
|
||||
AuthorizationURL *url.URL
|
||||
|
||||
// Token URL is the URL of the token endpoint that must be used for this
|
||||
// OAuth client, as defined in the OAuth2 specifications.
|
||||
TokenURL *url.URL
|
||||
|
||||
// MinPort and MaxPort define a range of TCP ports on localhost that this
|
||||
// client is able to use as redirect_uri in an authorization request.
|
||||
// Terraform will select a port from this range for the temporary HTTP
|
||||
// server it creates to receive the authorization response, giving
|
||||
// a URL like http://localhost:NNN/ where NNN is the selected port number.
|
||||
//
|
||||
// Terraform will reject any port numbers in this range less than 1024,
|
||||
// to respect the common convention (enforced on some operating systems)
|
||||
// that lower port numbers are reserved for "privileged" services.
|
||||
MinPort, MaxPort uint16
|
||||
}
|
||||
|
||||
// Endpoint returns an oauth2.Endpoint value ready to be used with the oauth2
|
||||
// library, representing the URLs from the receiver.
|
||||
func (c *OAuthClient) Endpoint() oauth2.Endpoint {
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: c.AuthorizationURL.String(),
|
||||
TokenURL: c.TokenURL.String(),
|
||||
|
||||
// We don't actually auth because we're not a server-based OAuth client,
|
||||
// so this instead just means that we include client_id as an argument
|
||||
// in our requests.
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue