blob: 86f57f4ec6f6fb37875739f9e3407fbd04741a4c [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.
package impl
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"sync"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/v23/security/access"
"v.io/v23/services/device"
"v.io/v23/services/pprof"
"v.io/v23/services/stats"
"v.io/v23/vdl"
"v.io/v23/vdlroot/signature"
"v.io/v23/verror"
"v.io/x/lib/vlog"
"v.io/x/ref"
"v.io/x/ref/services/agent/keymgr"
s_device "v.io/x/ref/services/device"
"v.io/x/ref/services/device/internal/config"
"v.io/x/ref/services/internal/logreaderlib"
"v.io/x/ref/services/internal/pathperms"
)
// internalState wraps state shared between different device manager
// invocations.
type internalState struct {
callback *callbackState
updating *updatingState
securityAgent *securityAgentState
restartHandler func()
testMode bool
}
// dispatcher holds the state of the device manager dispatcher.
type dispatcher struct {
// internal holds the state that persists across RPC method invocations.
internal *internalState
// config holds the device manager's (immutable) configuration state.
config *config.State
// dispatcherMutex is a lock for coordinating concurrent access to some
// dispatcher methods.
mu sync.RWMutex
// TODO(rjkroege): Consider moving this inside internal.
uat BlessingSystemAssociationStore
permsStore *pathperms.PathStore
// Namespace
mtAddress string // The address of the local mounttable.
// reap is the app process monitoring subsystem.
reap reaper
}
var _ rpc.Dispatcher = (*dispatcher)(nil)
const (
appsSuffix = "apps"
deviceSuffix = "device"
configSuffix = "cfg"
pkgPath = "v.io/x/ref/services/device/internal/impl"
)
var (
ErrInvalidSuffix = verror.Register(pkgPath+".InvalidSuffix", verror.NoRetry, "{1:}{2:} invalid suffix{:_}")
ErrOperationFailed = verror.Register(pkgPath+".OperationFailed", verror.NoRetry, "{1:}{2:} operation failed{:_}")
ErrOperationInProgress = verror.Register(pkgPath+".OperationInProgress", verror.NoRetry, "{1:}{2:} operation in progress{:_}")
ErrAppTitleMismatch = verror.Register(pkgPath+".AppTitleMismatch", verror.NoRetry, "{1:}{2:} app title mismatch{:_}")
ErrUpdateNoOp = verror.Register(pkgPath+".UpdateNoOp", verror.NoRetry, "{1:}{2:} update is no op{:_}")
ErrInvalidOperation = verror.Register(pkgPath+".InvalidOperation", verror.NoRetry, "{1:}{2:} invalid operation{:_}")
ErrInvalidBlessing = verror.Register(pkgPath+".InvalidBlessing", verror.NoRetry, "{1:}{2:} invalid blessing{:_}")
ErrInvalidPairingToken = verror.Register(pkgPath+".InvalidPairingToken", verror.NoRetry, "{1:}{2:} pairing token mismatch{:_}")
ErrUnclaimedDevice = verror.Register(pkgPath+".UnclaimedDevice", verror.NoRetry, "{1:}{2:} device needs to be claimed first")
ErrDeviceAlreadyClaimed = verror.Register(pkgPath+".AlreadyClaimed", verror.NoRetry, "{1:}{2:} device has already been claimed")
errInvalidConfig = verror.Register(pkgPath+".errInvalidConfig", verror.NoRetry, "{1:}{2:} invalid config {3}{:_}")
errCantCreateAccountStore = verror.Register(pkgPath+".errCantCreateAccountStore", verror.NoRetry, "{1:}{2:} cannot create persistent store for identity to system account associations{:_}")
errCantCreateAppWatcher = verror.Register(pkgPath+".errCantCreateAppWatcher", verror.NoRetry, "{1:}{2:} cannot create app status watcher{:_}")
errNewAgentFailed = verror.Register(pkgPath+".errNewAgentFailed", verror.NoRetry, "{1:}{2:} NewAgent() failed{:_}")
)
// NewClaimableDispatcher returns an rpc.Dispatcher that allows the device to
// be Claimed if it hasn't been already and a channel that will be closed once
// the device has been claimed.
//
// It returns (nil, nil) if the device is no longer claimable.
func NewClaimableDispatcher(ctx *context.T, config *config.State, pairingToken string) (rpc.Dispatcher, <-chan struct{}) {
var (
permsDir = PermsDir(config)
permsStore = pathperms.NewPathStore(v23.GetPrincipal(ctx))
)
if _, _, err := permsStore.Get(permsDir); !os.IsNotExist(err) {
return nil, nil
}
// The device is claimable only if Claim hasn't been called before. The
// existence of the Permissions file is an indication of a successful prior
// call to Claim.
notify := make(chan struct{})
return &claimable{token: pairingToken, permsStore: permsStore, permsDir: permsDir, notify: notify}, notify
}
// NewDispatcher is the device manager dispatcher factory.
func NewDispatcher(ctx *context.T, config *config.State, mtAddress string, testMode bool, restartHandler func(), permStore *pathperms.PathStore) (rpc.Dispatcher, error) {
if err := config.Validate(); err != nil {
return nil, verror.New(errInvalidConfig, ctx, config, err)
}
uat, err := NewBlessingSystemAssociationStore(config.Root)
if err != nil {
return nil, verror.New(errCantCreateAccountStore, ctx, err)
}
reap, err := newReaper(ctx, config.Root)
if err != nil {
return nil, verror.New(errCantCreateAppWatcher, ctx, err)
}
initSuidHelper(config.Helper)
d := &dispatcher{
internal: &internalState{
callback: newCallbackState(config.Name),
updating: newUpdatingState(),
restartHandler: restartHandler,
testMode: testMode,
},
config: config,
uat: uat,
permsStore: permStore,
mtAddress: mtAddress,
reap: reap,
}
// If we're in 'security agent mode', set up the key manager agent.
if len(os.Getenv(ref.EnvAgentEndpoint)) > 0 {
if keyMgrAgent, err := keymgr.NewAgent(); err != nil {
return nil, verror.New(errNewAgentFailed, ctx, err)
} else {
d.internal.securityAgent = &securityAgentState{
keyMgrAgent: keyMgrAgent,
}
}
}
if testMode {
return &testModeDispatcher{d}, nil
}
return d, nil
}
// Shutdown the dispatcher.
func Shutdown(rpcd rpc.Dispatcher) {
switch d := rpcd.(type) {
case *dispatcher:
d.reap.shutdown()
case *testModeDispatcher:
Shutdown(d.realDispatcher)
default:
vlog.Panicf("%v not a supported dispatcher type.", rpcd)
}
}
// Logging invoker that logs any error messages before returning.
func newLoggingInvoker(obj interface{}) (rpc.Invoker, error) {
if invoker, ok := obj.(rpc.Invoker); ok {
return &loggingInvoker{invoker}, nil
}
invoker, err := rpc.ReflectInvoker(obj)
if err != nil {
vlog.Errorf("rpc.ReflectInvoker returned error: %v", err)
return nil, err
}
return &loggingInvoker{invoker}, nil
}
type loggingInvoker struct {
invoker rpc.Invoker
}
func (l *loggingInvoker) Prepare(method string, numArgs int) (argptrs []interface{}, tags []*vdl.Value, err error) {
argptrs, tags, err = l.invoker.Prepare(method, numArgs)
if err != nil {
vlog.Errorf("Prepare(%s %d) returned error: %v", method, numArgs, err)
}
return
}
func (l *loggingInvoker) Invoke(ctx *context.T, call rpc.StreamServerCall, method string, argptrs []interface{}) (results []interface{}, err error) {
results, err = l.invoker.Invoke(ctx, call, method, argptrs)
if err != nil {
vlog.Errorf("Invoke(method:%s argptrs:%v) returned error: %v", method, argptrs, err)
}
return
}
func (l *loggingInvoker) Signature(ctx *context.T, call rpc.ServerCall) ([]signature.Interface, error) {
sig, err := l.invoker.Signature(ctx, call)
if err != nil {
vlog.Errorf("Signature returned error: %v", err)
}
return sig, err
}
func (l *loggingInvoker) MethodSignature(ctx *context.T, call rpc.ServerCall, method string) (signature.Method, error) {
methodSig, err := l.invoker.MethodSignature(ctx, call, method)
if err != nil {
vlog.Errorf("MethodSignature(%s) returned error: %v", method, err)
}
return methodSig, err
}
func (l *loggingInvoker) Globber() *rpc.GlobState {
return l.invoker.Globber()
}
// DISPATCHER INTERFACE IMPLEMENTATION
func (d *dispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
invoker, auth, err := d.internalLookup(suffix)
if err != nil {
return nil, nil, err
}
loggingInvoker, err := newLoggingInvoker(invoker)
if err != nil {
return nil, nil, err
}
return loggingInvoker, auth, nil
}
func newTestableHierarchicalAuth(testMode bool, rootDir, childDir string, get pathperms.PermsGetter) (security.Authorizer, error) {
if testMode {
// In test mode, the device manager will not be able to read the
// Permissions, because they were signed with the key of the real device
// manager. It's not a problem because the testModeDispatcher overrides the
// authorizer anyway.
return nil, nil
}
return pathperms.NewHierarchicalAuthorizer(rootDir, childDir, get)
}
func (d *dispatcher) internalLookup(suffix string) (interface{}, security.Authorizer, error) {
components := strings.Split(suffix, "/")
for i := 0; i < len(components); i++ {
if len(components[i]) == 0 {
components = append(components[:i], components[i+1:]...)
i--
}
}
// TODO(rjkroege): Permit the root Permissions to diverge for the device and
// app sub-namespaces of the device manager after claiming.
auth, err := newTestableHierarchicalAuth(d.internal.testMode, PermsDir(d.config), PermsDir(d.config), d.permsStore)
if err != nil {
return nil, nil, err
}
if len(components) == 0 {
return rpc.ChildrenGlobberInvoker(deviceSuffix, appsSuffix), auth, nil
}
// The implementation of the device manager is split up into several
// invokers, which are instantiated depending on the receiver name
// prefix.
switch components[0] {
case deviceSuffix:
receiver := device.DeviceServer(&deviceService{
callback: d.internal.callback,
updating: d.internal.updating,
restartHandler: d.internal.restartHandler,
config: d.config,
disp: d,
uat: d.uat,
securityAgent: d.internal.securityAgent,
})
return receiver, auth, nil
case appsSuffix:
// Requests to apps/*/*/*/logs are handled locally by LogFileService.
// Requests to apps/*/*/*/pprof are proxied to the apps' __debug/pprof object.
// Requests to apps/*/*/*/stats are proxied to the apps' __debug/stats object.
// Everything else is handled by the Application server.
if len(components) >= 5 {
appInstanceDir, err := instanceDir(d.config.Root, components[1:4])
if err != nil {
return nil, nil, err
}
switch kind := components[4]; kind {
case "logs":
logsDir := filepath.Join(appInstanceDir, "logs")
suffix := naming.Join(components[5:]...)
appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore)
if err != nil {
return nil, nil, err
}
return logreaderlib.NewLogFileService(logsDir, suffix), appSpecificAuthorizer, nil
case "pprof", "stats":
info, err := loadInstanceInfo(nil, appInstanceDir)
if err != nil {
return nil, nil, err
}
if !instanceStateIs(appInstanceDir, device.InstanceStateRunning) {
return nil, nil, verror.New(ErrInvalidSuffix, nil)
}
var desc []rpc.InterfaceDesc
switch kind {
case "pprof":
desc = pprof.PProfServer(nil).Describe__()
case "stats":
desc = stats.StatsServer(nil).Describe__()
}
suffix := naming.Join("__debug", naming.Join(components[4:]...))
remote := naming.JoinAddressName(info.AppCycleMgrName, suffix)
// Use hierarchical auth with debugacls under debug access.
appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore)
if err != nil {
return nil, nil, err
}
return newProxyInvoker(remote, access.Debug, desc), appSpecificAuthorizer, nil
}
}
receiver := device.ApplicationServer(&appService{
config: d.config,
suffix: components[1:],
uat: d.uat,
permsStore: d.permsStore,
reap: d.reap,
appStart: &appStartState{
callback: d.internal.callback,
securityAgent: d.internal.securityAgent,
mtAddress: d.mtAddress,
},
})
appSpecificAuthorizer, err := newAppSpecificAuthorizer(auth, d.config, components[1:], d.permsStore)
if err != nil {
return nil, nil, err
}
return receiver, appSpecificAuthorizer, nil
case configSuffix:
if len(components) != 2 {
return nil, nil, verror.New(ErrInvalidSuffix, nil)
}
receiver := s_device.ConfigServer(&configService{
callback: d.internal.callback,
suffix: components[1],
})
// The nil authorizer ensures that only principals blessed by
// the device manager can talk back to it. All apps started by
// the device manager should fall in that category.
//
// TODO(caprita,rjkroege): We should further refine this, by
// only allowing the app to update state referring to itself
// (and not other apps).
return receiver, nil, nil
default:
return nil, nil, verror.New(ErrInvalidSuffix, nil)
}
}
// testModeDispatcher is a wrapper around the real dispatcher. It returns the
// exact same object as the real dispatcher, but the authorizer only allows
// calls to "device".Delete().
type testModeDispatcher struct {
realDispatcher rpc.Dispatcher
}
func (d *testModeDispatcher) Lookup(suffix string) (interface{}, security.Authorizer, error) {
obj, _, err := d.realDispatcher.Lookup(suffix)
return obj, d, err
}
func (testModeDispatcher) Authorize(ctx *context.T, call security.Call) error {
if call.Suffix() == deviceSuffix && call.Method() == "Delete" {
vlog.Infof("testModeDispatcher.Authorize: Allow %q.%s()", call.Suffix(), call.Method())
return nil
}
vlog.Infof("testModeDispatcher.Authorize: Reject %q.%s()", call.Suffix(), call.Method())
return verror.New(ErrInvalidSuffix, nil)
}
func newAppSpecificAuthorizer(sec security.Authorizer, config *config.State, suffix []string, getter pathperms.PermsGetter) (security.Authorizer, error) {
// TODO(rjkroege): This does not support <appname>.Start() to start all
// instances. Correct this.
// If we are attempting a method invocation against "apps/", we use the root
// Permissions.
if len(suffix) == 0 || len(suffix) == 1 {
return sec, nil
}
// Otherwise, we require a per-installation and per-instance Permissions file.
if len(suffix) == 2 {
p, err := installationDirCore(suffix, config.Root)
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err))
}
return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter)
}
// Use the special debugacls for instance/logs, instance/pprof, instance/stats.
if len(suffix) > 3 && (suffix[3] == "logs" || suffix[3] == "pprof" || suffix[3] == "stats") {
p, err := instanceDir(config.Root, suffix[0:3])
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err))
}
return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "debugacls"), getter)
}
p, err := instanceDir(config.Root, suffix[0:3])
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("newAppSpecificAuthorizer failed: %v", err))
}
return pathperms.NewHierarchicalAuthorizer(PermsDir(config), path.Join(p, "acls"), getter)
}