blob: ce9661f3a8324406c759d29ae741653347b3d172 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build mojo
// The evolving story of identification and security in syncbase
// running in mojo:
// (Last Updated: November 5, 2015).
//
// SHORT STORY:
// The syncbase mojo service gets a blessing by exchanging an OAuth2 token for
// a blessing. This allows syncbases to identify each other when synchronizing
// data between each other and enables basic security policies like "all my
// devices sync all my data with each other". However, different mojo "clients"
// (that communicate with the local syncbase via mojo IPC) are not isolated
// from each other - so each mojo app has complete access to all the data
// written to the local syncbase service (modulo any filesystem isolation via
// the --root-dir flag). This is something that we intend to change.
//
// LONGER VERSION:
// A "mojo app" consists of a set of interrelated components, accessed by a
// URL. Communication between these components is via mojo IPC. One of those
// components is syncbase (this package), which is a store that synchronizes
// between devices via Vanadium RPCs. See:
// https://docs.google.com/a/google.com/document/d/1TyxPYIhj9VBCtY7eAXu_MEV9y0dtRx7n7UY4jm76Qq4/edit?usp=sharing
//
// Any process communicating via Vanadium RPCs needs to have an identity
// represented as a Blessing
// (https://github.com/vanadium/docs/blob/master/concepts/security.md).
//
// So, the current story is this:
// 1 The mojo syncbase app/service fetches an OAuth2 token from the
// mojo authentication service
// 2 It exchanges this token for a blessing bound to the public
// key of the principal associated with this service
// (this is essentially a copy of what the mojo principal service
// implementation does)
// 3 And all RPCs use the blessing obtained above
// 4 Other mojo components communicate with their local syncbase via
// Mojo IPC (see v.io/x/ref/services/syncbase/server/mojo_call.go)
// and DO NOT AUTHENTICATE with it
// 5 The resulting security model is that all mojo apps on the same
// device have access to whatever is stored in the local syncbase
//
// Clearly (5) is not ideal. And as this evolves, the plan is likely to be
// something involving each "client" of this syncbase mojo service being
// identified using the principal service
// (https://github.com/domokit/mojo/blob/master/mojo/services/vanadium/security/interfaces/principal.mojom).
// But more on that when we get there.
package main
import (
"encoding/base64"
"fmt"
"mojo/public/go/application"
"mojo/public/go/bindings"
"mojo/public/go/system"
"mojo/public/interfaces/network/url_request"
"mojo/services/authentication/interfaces/authentication"
"mojo/services/network/interfaces/network_service"
"mojo/services/network/interfaces/url_loader"
"net/url"
"runtime"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/security"
"v.io/v23/vom"
seclib "v.io/x/ref/lib/security"
)
type selectAccountFailed struct {
error
}
func setBlessings(v23ctx *context.T, appctx application.Context) error {
// Get an OAuth2 token from the mojo authentication service.
// At the time of this writing, the mojo authentication
// service was implemented only for Android, so in absence
// of that abort. Which means that the syncbases cannot
// communicate with each other, unless --v23.credentials was
// supplied as a flag.
if runtime.GOOS != "android" {
v23ctx.Infof("Using default blessings for non-Android OS (%v)", runtime.GOOS)
return nil
}
// TODO(ashankar,ataly): This is almost a duplicate of what
// is in
// https://github.com/domokit/mojo/blob/master/services/vanadium/security/principal_service.go
// - at some point this needs to be cleared up and this
// syncbase should use the principal service?
token, err := oauthToken(appctx)
if err != nil {
if _, ok := err.(selectAccountFailed); ok {
// TODO(sadovsky): This behavior is convenient for development, but
// probably ought to be disabled in production.
v23ctx.Infof("SelectAccount failed; using default blessings")
return nil
}
return err
}
v23ctx.Infof("Obtained OAuth2 token, will exchange for blessings")
p := v23.GetPrincipal(v23ctx)
blessings, err := token2blessings(appctx, token, p.PublicKey())
if err != nil {
return err
}
v23ctx.Infof("Obtained blessings %v", blessings)
if err := seclib.SetDefaultBlessings(p, blessings); err != nil {
return fmt.Errorf("failed to use blessings: %v", err)
}
return nil
}
func oauthToken(ctx application.Context) (string, error) {
req, ptr := authentication.CreateMessagePipeForAuthenticationService()
ctx.ConnectToApplication("mojo:authentication").ConnectToService(&req)
proxy := authentication.NewAuthenticationServiceProxy(ptr, bindings.GetAsyncWaiter())
name, errstr, _ := proxy.SelectAccount(true /*return last selected*/)
if name == nil || errstr != nil {
return "", selectAccountFailed{error: fmt.Errorf("failed to select an account for user: %v", errstr)}
}
token, errstr, _ := proxy.GetOAuth2Token(*name, []string{"email"})
if token == nil || errstr != nil {
return "", fmt.Errorf("failed to obtain an OAuth2 token for %q: %v", *name, errstr)
}
return *token, nil
}
func token2blessings(ctx application.Context, token string, key security.PublicKey) (security.Blessings, error) {
var ret security.Blessings
netReq, netPtr := network_service.CreateMessagePipeForNetworkService()
ctx.ConnectToApplication("mojo:network_service").ConnectToService(&netReq)
netProxy := network_service.NewNetworkServiceProxy(netPtr, bindings.GetAsyncWaiter())
loaderReq, loaderPtr := url_loader.CreateMessagePipeForUrlLoader()
if err := netProxy.CreateUrlLoader(loaderReq); err != nil {
return ret, fmt.Errorf("failed to create URL loader: %v", err)
}
url, err := token2blessingURL(token, key)
if err != nil {
return ret, err
}
response, err := url_loader.NewUrlLoaderProxy(loaderPtr, bindings.GetAsyncWaiter()).Start(url_request.UrlRequest{
Url: url,
Method: "GET",
})
if err != nil || response.Error != nil {
var errrep interface{}
if err != nil {
errrep = err
} else {
errrep = response.Error
}
return ret, fmt.Errorf("HTTP request to exchange OAuth token for blessings failed: %v", errrep)
}
res, b64bytes := (*response.Body).ReadData(system.MOJO_READ_DATA_FLAG_ALL_OR_NONE)
if res != system.MOJO_RESULT_OK {
return ret, fmt.Errorf("failed to read blessings from the Vanadium identity provider. Result: %v", res)
}
vombytes, err := base64.URLEncoding.DecodeString(string(b64bytes))
if err != nil {
return ret, fmt.Errorf("invalid base64 encoded blessings: %v", err)
}
if err := vom.Decode(vombytes, &ret); err != nil {
return ret, fmt.Errorf("invalid encoded blessings: %v", err)
}
return ret, nil
}
func token2blessingURL(token string, key security.PublicKey) (string, error) {
base, err := url.Parse("https://dev.v.io/auth/google/bless")
if err != nil {
return "", fmt.Errorf("failed to parse blessing URL: %v", err)
}
pub, err := key.MarshalBinary()
if err != nil {
return "", fmt.Errorf("invalid public key: %v", err)
}
params := url.Values{}
params.Add("public_key", base64.URLEncoding.EncodeToString(pub))
params.Add("token", token)
params.Add("output_format", "base64vom")
base.RawQuery = params.Encode()
return base.String(), nil
}