blob: 2ad5dcbe7512540073f56a41df72040d7bbc5610 [file] [log] [blame]
// HTTP server that uses OAuth to create security.Blessings objects.
package server
import (
"crypto/rand"
"fmt"
"html/template"
"net"
"net/http"
"reflect"
"strings"
"v.io/core/veyron2"
"v.io/core/veyron2/ipc"
"v.io/core/veyron2/naming"
"v.io/core/veyron2/options"
"v.io/core/veyron2/rt"
"v.io/core/veyron2/security"
verror "v.io/core/veyron2/verror2"
"v.io/core/veyron2/vlog"
"v.io/core/veyron/lib/signals"
"v.io/core/veyron/security/audit"
"v.io/core/veyron/services/identity/auditor"
"v.io/core/veyron/services/identity/blesser"
"v.io/core/veyron/services/identity/caveats"
"v.io/core/veyron/services/identity/handlers"
"v.io/core/veyron/services/identity/oauth"
"v.io/core/veyron/services/identity/revocation"
services "v.io/core/veyron/services/security"
"v.io/core/veyron/services/security/discharger"
)
const (
oauthBlesserService = "google"
macaroonService = "macaroon"
dischargerService = "discharger"
)
type identityd struct {
oauthProvider oauth.OAuthProvider
auditor audit.Auditor
blessingLogReader auditor.BlessingLogReader
revocationManager revocation.RevocationManager
oauthBlesserParams blesser.GoogleParams
caveatSelector caveats.CaveatSelector
}
// NewIdentityServer returns a IdentityServer that:
// - uses oauthProvider to authenticate users
// - auditor and blessingLogReader to audit the root principal and read audit logs
// - revocationManager to store revocation data and grant discharges
// - oauthBlesserParams to configure the identity.OAuthBlesser service
func NewIdentityServer(oauthProvider oauth.OAuthProvider, auditor audit.Auditor, blessingLogReader auditor.BlessingLogReader, revocationManager revocation.RevocationManager, oauthBlesserParams blesser.GoogleParams, caveatSelector caveats.CaveatSelector) *identityd {
return &identityd{
oauthProvider,
auditor,
blessingLogReader,
revocationManager,
oauthBlesserParams,
caveatSelector,
}
}
func (s *identityd) Serve(listenSpec *ipc.ListenSpec, host, httpaddr, tlsconfig string) {
p, r := providerPrincipal(s.auditor)
defer r.Cleanup()
runtime, err := rt.New(options.RuntimePrincipal{p})
if err != nil {
vlog.Fatal(err)
}
defer runtime.Cleanup()
ipcServer, _, _ := s.Listen(runtime, listenSpec, host, httpaddr, tlsconfig)
defer ipcServer.Stop()
<-signals.ShutdownOnSignals(runtime)
}
func (s *identityd) Listen(runtime veyron2.Runtime, listenSpec *ipc.ListenSpec, host, httpaddr, tlsconfig string) (ipc.Server, []string, string) {
// Setup handlers
http.Handle("/blessing-root", handlers.BlessingRoot{runtime.Principal()}) // json-encoded public key and blessing names of this server
macaroonKey := make([]byte, 32)
if _, err := rand.Read(macaroonKey); err != nil {
vlog.Fatalf("macaroonKey generation failed: %v", err)
}
ipcServer, published, err := s.setupServices(runtime, listenSpec, macaroonKey)
if err != nil {
vlog.Fatalf("Failed to setup veyron services for blessing: %v", err)
}
externalHttpaddr := httpaddress(host, httpaddr)
n := "/google/"
h, err := oauth.NewHandler(oauth.HandlerArgs{
R: runtime,
MacaroonKey: macaroonKey,
Addr: fmt.Sprintf("%s%s", externalHttpaddr, n),
BlessingLogReader: s.blessingLogReader,
RevocationManager: s.revocationManager,
DischargerLocation: naming.JoinAddressName(published[0], dischargerService),
MacaroonBlessingService: naming.JoinAddressName(published[0], macaroonService),
OAuthProvider: s.oauthProvider,
CaveatSelector: s.caveatSelector,
})
if err != nil {
vlog.Fatalf("Failed to create HTTP handler for oauth authentication: %v", err)
}
http.Handle(n, h)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
args := struct {
Self security.Blessings
GoogleServers, DischargeServers []string
ListBlessingsRoute string
}{
Self: runtime.Principal().BlessingStore().Default(),
}
if s.revocationManager != nil {
args.DischargeServers = appendSuffixTo(published, dischargerService)
}
var emptyParams blesser.GoogleParams
if !reflect.DeepEqual(s.oauthBlesserParams, emptyParams) {
args.GoogleServers = appendSuffixTo(published, oauthBlesserService)
}
if s.blessingLogReader != nil {
args.ListBlessingsRoute = oauth.ListBlessingsRoute
}
if err := tmpl.Execute(w, args); err != nil {
vlog.Info("Failed to render template:", err)
}
})
vlog.Infof("Running HTTP server at: %v", externalHttpaddr)
go runHTTPSServer(httpaddr, tlsconfig)
return ipcServer, published, externalHttpaddr
}
func appendSuffixTo(objectname []string, suffix string) []string {
names := make([]string, len(objectname))
for i, o := range objectname {
names[i] = naming.JoinAddressName(o, suffix)
}
return names
}
// Starts the blessing services and the discharging service on the same port.
func (s *identityd) setupServices(runtime veyron2.Runtime, listenSpec *ipc.ListenSpec, macaroonKey []byte) (ipc.Server, []string, error) {
server, err := runtime.NewServer()
if err != nil {
return nil, nil, fmt.Errorf("failed to create new ipc.Server: %v", err)
}
eps, err := server.Listen(*listenSpec)
if err != nil {
return nil, nil, fmt.Errorf("server.Listen(%v) failed: %v", *listenSpec, err)
}
ep := eps[0]
dispatcher := newDispatcher(macaroonKey, oauthBlesserParams(s.oauthBlesserParams, s.revocationManager, ep))
objectname := naming.Join("identity", fmt.Sprintf("%v", runtime.Principal().BlessingStore().Default()))
if err := server.ServeDispatcher(objectname, dispatcher); err != nil {
return nil, nil, fmt.Errorf("failed to start Veyron services: %v", err)
}
vlog.Infof("Blessing and discharger services enabled at %v", naming.JoinAddressName(ep.String(), objectname))
published, _ := server.Published()
if len(published) == 0 {
// No addresses published, publish the endpoint instead (which may not be usable everywhere, but oh-well).
published = append(published, ep.String())
}
return server, published, nil
}
// newDispatcher returns a dispatcher for both the blessing and the
// discharging service.
func newDispatcher(macaroonKey []byte, blesserParams blesser.GoogleParams) ipc.Dispatcher {
d := dispatcher(map[string]interface{}{
macaroonService: blesser.NewMacaroonBlesserServer(macaroonKey),
dischargerService: services.DischargerServer(discharger.NewDischarger()),
oauthBlesserService: blesser.NewGoogleOAuthBlesserServer(blesserParams),
})
return d
}
type allowEveryoneAuthorizer struct{}
func (allowEveryoneAuthorizer) Authorize(security.Context) error { return nil }
type dispatcher map[string]interface{}
func (d dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
if invoker := d[suffix]; invoker != nil {
return invoker, allowEveryoneAuthorizer{}, nil
}
return nil, nil, verror.Make(verror.NoExist, nil, suffix)
}
func oauthBlesserParams(inputParams blesser.GoogleParams, revocationManager revocation.RevocationManager, ep naming.Endpoint) blesser.GoogleParams {
inputParams.DischargerLocation = naming.JoinAddressName(ep.String(), dischargerService)
return inputParams
}
func runHTTPSServer(addr, tlsconfig string) {
if len(tlsconfig) == 0 {
vlog.Fatal("Please set the --tlsconfig flag")
}
paths := strings.Split(tlsconfig, ",")
if len(paths) != 2 {
vlog.Fatalf("Could not parse --tlsconfig. Must have exactly two components, separated by a comma")
}
vlog.Infof("Starting HTTP server with TLS using certificate [%s] and private key [%s] at https://%s", paths[0], paths[1], addr)
if err := http.ListenAndServeTLS(addr, paths[0], paths[1], nil); err != nil {
vlog.Fatalf("http.ListenAndServeTLS failed: %v", err)
}
}
// providerPrincipal returns the Principal to use for the identity provider (i.e., this program).
//
// TODO(ataly, suharhs, mattr): HACK!!! This method also returns the runtime that it creates
// internally to read the principal supplied by the environment. This runtime must be cleaned up
// whenever identity server is shutdown. The runtime cannot be cleaned up here as the server may
// be running under an agent in which case cleaning up the runtime closes the connection to the
// agent. Therefore we return the runtime so that it can be cleaned up eventually. This problem
// would hopefully go away once we change the runtime to a context.T and have mechanisms for
// constructing and managing derived context.Ts.
func providerPrincipal(auditor audit.Auditor) (security.Principal, veyron2.Runtime) {
// TODO(ashankar): Somewhat silly to have to create a runtime, but oh-well.
r, err := rt.New()
if err != nil {
vlog.Fatal(err)
}
return audit.NewPrincipal(r.Principal(), auditor), r
}
func httpaddress(host, httpaddr string) string {
_, port, err := net.SplitHostPort(httpaddr)
if err != nil {
vlog.Fatalf("Failed to parse %q: %v", httpaddr, err)
}
return fmt.Sprintf("https://%s:%v", host, port)
}
var tmpl = template.Must(template.New("main").Parse(`<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Veyron Identity Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<div class="page-header"><h2>{{.Self}}</h2><h4>A Veyron Blessing Provider</h4></div>
<div class="well">
This is a Veyron identity provider that provides blessings with the name prefix <mark>{{.Self}}</mark>.
<br/>
The public key of this provider is {{.Self.PublicKey}}.
<br/>
The root names and public key (in DER encoded <a href="http://en.wikipedia.org/wiki/X.690#DER_encoding">format</a>)
are available in a <a class="btn btn-xs btn-primary" href="/blessing-root">JSON</a> object.
</div>
<div class="well">
<ul>
{{if .GoogleServers}}
<li>Blessings (using Google OAuth to fetch an email address) are provided via Veyron RPCs to: <tt>{{range .GoogleServers}}{{.}}{{end}}</tt></li>
{{end}}
{{if .DischargeServers}}
<li>RevocationCaveat Discharges are provided via Veyron RPCs to: <tt>{{range .DischargeServers}}{{.}}{{end}}</tt></li>
{{end}}
{{if .ListBlessingsRoute}}
<li>You can <a class="btn btn-xs btn-primary" href="/google/{{.ListBlessingsRoute}}">enumerate</a> blessings provided with your
email address.</li>
{{end}}
</ul>
</div>
</div>
</body>
</html>`))